Fix linking

This commit is contained in:
Ali 2020-02-27 00:02:03 +04:00
parent e6b96f6cd4
commit 461c278867
92 changed files with 19935 additions and 2 deletions

View File

@ -6,4 +6,7 @@ build --swiftcopt='-Xcc'
build --swiftcopt='-w'
build --spawn_strategy=local
build --strategy=SwiftCompile=local
build --features=debug_prefix_map_pwd_is_dot
build --features=swift.cacheable_swiftmodules
build --features=swift.debug_prefix_map

3
.gitmodules vendored
View File

@ -14,3 +14,6 @@
[submodule "submodules/TgVoip/libtgvoip"]
path = submodules/TgVoip/libtgvoip
url = https://github.com/telegramdesktop/libtgvoip.git
[submodule "build-system/tulsi"]
path = build-system/tulsi
url = https://github.com/ali-fareed/tulsi.git

View File

@ -36,6 +36,17 @@ GEN_DIRECTORY="build-input/gen/project"
rm -rf "$GEN_DIRECTORY"
mkdir -p "$GEN_DIRECTORY"
pushd "build-system/tulsi"
"$BAZEL" build //:tulsi --xcode_version=$(cat "build-system/xcode_version")
popd
TULSI_DIRECTORY="build-input/gen/project"
TULSI_APP="build-input/gen/project/Tulsi.app"
TULSI="$TULSI_APP/Contents/MacOS/Tulsi"
mkdir -p "$TULSI_DIRECTORY"
unzip -oq "build-system/tulsi/bazel-bin/tulsi.zip" -d "$TULSI_DIRECTORY"
CORE_COUNT=$(sysctl -n hw.logicalcpu)
CORE_COUNT_MINUS_ONE=$(expr ${CORE_COUNT} \- 1)
@ -51,7 +62,7 @@ if [ "$BAZEL_CACHE_DIR" != "" ]; then
BAZEL_OPTIONS=("${BAZEL_OPTIONS[@]}" --disk_cache="$(echo $BAZEL_CACHE_DIR | sed -e 's/[\/&]/\\&/g')")
fi
$HOME/Applications/Tulsi.app/Contents/MacOS/Tulsi -- \
"$TULSI" -- \
--verbose \
--create-tulsiproj Telegram \
--workspaceroot ./ \
@ -68,7 +79,7 @@ done
sed -i "" -e '1h;2,$H;$!d;g' -e 's/\("sourceFilters" : \[\n[ ]*\)"\.\/\.\.\."/\1"Telegram\/...", "submodules\/..."/' "$GEN_DIRECTORY/Telegram.tulsiproj/Configs/Telegram.tulsigen"
${HOME}/Applications/Tulsi.app/Contents/MacOS/Tulsi -- \
"$TULSI" -- \
--verbose \
--genconfig "$GEN_DIRECTORY/Telegram.tulsiproj:Telegram" \
--bazel "$BAZEL" \

1
build-system/tulsi Submodule

@ -0,0 +1 @@
Subproject commit ee185c4c20ea4384bc3cbf8ccd8705c904154abb

View File

@ -0,0 +1 @@
11.3.1

View File

@ -0,0 +1,21 @@
//
// ASAbstractLayoutController+FrameworkPrivate.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
//
// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode.
// These methods must never be called or overridden by other classes.
//
#include <vector>
@interface ASAbstractLayoutController (FrameworkPrivate)
+ (std::vector<std::vector<ASRangeTuningParameters>>)defaultTuningParameters;
@end

View File

@ -0,0 +1,186 @@
//
// ASAsciiArtBoxCreator.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAsciiArtBoxCreator.h>
#import <CoreGraphics/CoreGraphics.h>
#import <cmath>
static const NSUInteger kDebugBoxPadding = 2;
typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation)
{
PIDebugBoxPaddingLocationFront,
PIDebugBoxPaddingLocationEnd,
PIDebugBoxPaddingLocationBoth
};
@interface NSString(PIDebugBox)
@end
@implementation NSString(PIDebugBox)
+ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount NS_RETURNS_RETAINED
{
NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount];
for (NSUInteger index = 0; index < repeatCount; index++) {
[string appendString:stringToRepeat];
}
return [string copy];
}
- (NSString *)debugbox_stringByAddingPadding:(NSString *)padding count:(NSUInteger)count location:(PIDebugBoxPaddingLocation)location
{
NSString *paddingString = [NSString debugbox_stringWithString:padding repeatedCount:count];
switch (location) {
case PIDebugBoxPaddingLocationFront:
return [NSString stringWithFormat:@"%@%@", paddingString, self];
case PIDebugBoxPaddingLocationEnd:
return [NSString stringWithFormat:@"%@%@", self, paddingString];
case PIDebugBoxPaddingLocationBoth:
return [NSString stringWithFormat:@"%@%@%@", paddingString, self, paddingString];
}
return [self copy];
}
@end
@implementation ASAsciiArtBoxCreator
+ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent
{
if ([children count] == 0) {
return parent;
}
NSMutableArray *childrenLines = [NSMutableArray array];
// split the children into lines
NSUInteger lineCountPerChild = 0;
for (NSString *child in children) {
NSArray *lines = [child componentsSeparatedByString:@"\n"];
lineCountPerChild = MAX(lineCountPerChild, [lines count]);
}
for (NSString *child in children) {
NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy];
NSUInteger topPadding = ceil((CGFloat)(lineCountPerChild - [lines count])/2.0);
NSUInteger bottomPadding = (lineCountPerChild - [lines count])/2.0;
NSUInteger lineLength = [lines[0] length];
for (NSUInteger index = 0; index < topPadding; index++) {
[lines insertObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength] atIndex:0];
}
for (NSUInteger index = 0; index < bottomPadding; index++) {
[lines addObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength]];
}
[childrenLines addObject:lines];
}
NSMutableArray *concatenatedLines = [NSMutableArray array];
NSString *padding = [NSString debugbox_stringWithString:@" " repeatedCount:kDebugBoxPadding];
for (NSUInteger index = 0; index < lineCountPerChild; index++) {
NSMutableString *line = [[NSMutableString alloc] init];
[line appendFormat:@"|%@",padding];
for (NSArray *childLines in childrenLines) {
[line appendFormat:@"%@%@", childLines[index], padding];
}
[line appendString:@"|"];
[concatenatedLines addObject:line];
}
// surround the lines in a box
NSUInteger totalLineLength = [concatenatedLines[0] length];
if (totalLineLength < [parent length]) {
NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - totalLineLength;
NSUInteger leftPadding = ceil((CGFloat)difference/2.0);
NSUInteger rightPadding = difference/2;
NSString *leftString = [@"|" debugbox_stringByAddingPadding:@" " count:leftPadding location:PIDebugBoxPaddingLocationEnd];
NSString *rightString = [@"|" debugbox_stringByAddingPadding:@" " count:rightPadding location:PIDebugBoxPaddingLocationFront];
NSMutableArray *paddedLines = [NSMutableArray array];
for (NSString *line in concatenatedLines) {
NSString *paddedLine = [line stringByReplacingOccurrencesOfString:@"|" withString:leftString options:NSCaseInsensitiveSearch range:NSMakeRange(0, 1)];
paddedLine = [paddedLine stringByReplacingOccurrencesOfString:@"|" withString:rightString options:NSCaseInsensitiveSearch range:NSMakeRange([paddedLine length] - 1, 1)];
[paddedLines addObject:paddedLine];
}
concatenatedLines = paddedLines;
// totalLineLength += difference;
}
concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent];
return [concatenatedLines componentsJoinedByString:@"\n"];
}
+ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent
{
if ([children count] == 0) {
return parent;
}
NSMutableArray *childrenLines = [NSMutableArray array];
NSUInteger maxChildLength = 0;
for (NSString *child in children) {
NSArray *lines = [child componentsSeparatedByString:@"\n"];
maxChildLength = MAX(maxChildLength, [lines[0] length]);
}
NSUInteger rightPadding = 0;
NSUInteger leftPadding = 0;
if (maxChildLength < [parent length]) {
NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - maxChildLength;
leftPadding = ceil((CGFloat)difference/2.0);
rightPadding = difference/2;
}
NSString *rightPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightPadding + kDebugBoxPadding];
NSString *leftPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftPadding + kDebugBoxPadding];
for (NSString *child in children) {
NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy];
NSUInteger leftLinePadding = ceil((CGFloat)(maxChildLength - [lines[0] length])/2.0);
NSUInteger rightLinePadding = (maxChildLength - [lines[0] length])/2.0;
for (NSString *line in lines) {
NSString *rightLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightLinePadding];
rightLinePaddingString = [NSString stringWithFormat:@"%@%@|", rightLinePaddingString, rightPaddingString];
NSString *leftLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftLinePadding];
leftLinePaddingString = [NSString stringWithFormat:@"|%@%@", leftLinePaddingString, leftPaddingString];
NSString *paddingLine = [NSString stringWithFormat:@"%@%@%@", leftLinePaddingString, line, rightLinePaddingString];
[childrenLines addObject:paddingLine];
}
}
childrenLines = [self appendTopAndBottomToBoxString:childrenLines parent:parent];
return [childrenLines componentsJoinedByString:@"\n"];
}
+ (NSMutableArray *)appendTopAndBottomToBoxString:(NSMutableArray *)boxStrings parent:(NSString *)parent
{
NSUInteger totalLineLength = [boxStrings[0] length];
[boxStrings addObject:[NSString debugbox_stringWithString:@"-" repeatedCount:totalLineLength]];
NSUInteger leftPadding = ceil(((CGFloat)(totalLineLength - [parent length]))/2.0);
NSUInteger rightPadding = (totalLineLength - [parent length])/2;
NSString *topLine = [parent debugbox_stringByAddingPadding:@"-" count:leftPadding location:PIDebugBoxPaddingLocationFront];
topLine = [topLine debugbox_stringByAddingPadding:@"-" count:rightPadding location:PIDebugBoxPaddingLocationEnd];
[boxStrings insertObject:topLine atIndex:0];
return boxStrings;
}
@end

View File

@ -0,0 +1,58 @@
//
// ASAssert.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASAvailability.h>
#if AS_TLS_AVAILABLE
static _Thread_local int tls_mainThreadAssertionsDisabledCount;
BOOL ASMainThreadAssertionsAreDisabled() {
return tls_mainThreadAssertionsDisabledCount > 0;
}
void ASPushMainThreadAssertionsDisabled() {
tls_mainThreadAssertionsDisabledCount += 1;
}
void ASPopMainThreadAssertionsDisabled() {
tls_mainThreadAssertionsDisabledCount -= 1;
ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push.");
}
#else
#import <dispatch/once.h>
static pthread_key_t ASMainThreadAssertionsDisabledKey() {
static pthread_key_t k;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_key_create(&k, NULL);
});
return k;
}
BOOL ASMainThreadAssertionsAreDisabled() {
return (nullptr != pthread_getspecific(ASMainThreadAssertionsDisabledKey()));
}
void ASPushMainThreadAssertionsDisabled() {
const auto key = ASMainThreadAssertionsDisabledKey();
const auto oldVal = (intptr_t)pthread_getspecific(key);
pthread_setspecific(key, (void *)(oldVal + 1));
}
void ASPopMainThreadAssertionsDisabled() {
const auto key = ASMainThreadAssertionsDisabledKey();
const auto oldVal = (intptr_t)pthread_getspecific(key);
pthread_setspecific(key, (void *)(oldVal - 1));
ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push.");
}
#endif // AS_TLS_AVAILABLE

View File

@ -0,0 +1,28 @@
//
// ASCGImageBuffer.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <CoreGraphics/CGDataProvider.h>
NS_ASSUME_NONNULL_BEGIN
AS_SUBCLASSING_RESTRICTED
@interface ASCGImageBuffer : NSObject
/// Init a zero-filled buffer with the given length.
- (instancetype)initWithLength:(NSUInteger)length;
@property (readonly) void *mutableBytes NS_RETURNS_INNER_POINTER;
/// Don't do any drawing or call any methods after calling this.
- (CGDataProviderRef)createDataProviderAndInvalidate CF_RETURNS_RETAINED;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,88 @@
//
// ASCGImageBuffer.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASCGImageBuffer.h"
#import <sys/mman.h>
#import <mach/mach_init.h>
#import <mach/vm_map.h>
#import <mach/vm_statistics.h>
/**
* The behavior of this class is modeled on the private function
* _CGDataProviderCreateWithCopyOfData, which is the function used
* by CGBitmapContextCreateImage.
*
* If the buffer is larger than a page, we use mmap and mark it as
* read-only when they are finished drawing. Then we wrap the VM
* in an NSData
*/
@implementation ASCGImageBuffer {
BOOL _createdData;
BOOL _isVM;
NSUInteger _length;
}
- (instancetype)initWithLength:(NSUInteger)length
{
if (self = [super init]) {
_length = length;
_isVM = (length >= vm_page_size);
if (_isVM) {
_mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0);
if (_mutableBytes == MAP_FAILED) {
NSAssert(NO, @"Failed to map for CG image data.");
_isVM = NO;
}
}
// Check the VM flag again because we may have failed above.
if (!_isVM) {
_mutableBytes = calloc(1, length);
}
}
return self;
}
- (void)dealloc
{
if (!_createdData) {
[ASCGImageBuffer deallocateBuffer:_mutableBytes length:_length isVM:_isVM];
}
}
- (CGDataProviderRef)createDataProviderAndInvalidate
{
NSAssert(!_createdData, @"Should not create data provider from buffer multiple times.");
_createdData = YES;
// Mark the pages as read-only.
if (_isVM) {
__unused kern_return_t result = vm_protect(mach_task_self(), (vm_address_t)_mutableBytes, _length, true, VM_PROT_READ);
NSAssert(result == noErr, @"Error marking buffer as read-only: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]);
}
// Wrap in an NSData
BOOL isVM = _isVM;
NSData *d = [[NSData alloc] initWithBytesNoCopy:_mutableBytes length:_length deallocator:^(void * _Nonnull bytes, NSUInteger length) {
[ASCGImageBuffer deallocateBuffer:bytes length:length isVM:isVM];
}];
return CGDataProviderCreateWithCFData((__bridge CFDataRef)d);
}
+ (void)deallocateBuffer:(void *)buf length:(NSUInteger)length isVM:(BOOL)isVM
{
if (isVM) {
__unused kern_return_t result = vm_deallocate(mach_task_self(), (vm_address_t)buf, length);
NSAssert(result == noErr, @"Failed to unmap cg image buffer: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]);
} else {
free(buf);
}
}
@end

View File

@ -0,0 +1,61 @@
//
// ASCollections.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASCollections.h>
/**
* A private allocator that signals to our retain callback to skip the retain.
* It behaves the same as the default allocator, but acts as a signal that we
* are creating a transfer array so we should skip the retain.
*/
static CFAllocatorRef gTransferAllocator;
static const void *ASTransferRetain(CFAllocatorRef allocator, const void *val) {
if (allocator == gTransferAllocator) {
// Transfer allocator. Ignore retain and pass through.
return val;
} else {
// Other allocator. Retain like normal.
// This happens when they make a mutable copy.
return (&kCFTypeArrayCallBacks)->retain(allocator, val);
}
}
@implementation NSArray (ASCollections)
+ (NSArray *)arrayByTransferring:(__strong id *)pointers count:(NSUInteger)count NS_RETURNS_RETAINED
{
// Custom callbacks that point to our ASTransferRetain callback.
static CFArrayCallBacks callbacks;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
callbacks = kCFTypeArrayCallBacks;
callbacks.retain = ASTransferRetain;
CFAllocatorContext ctx;
CFAllocatorGetContext(NULL, &ctx);
gTransferAllocator = CFAllocatorCreate(NULL, &ctx);
});
// NSZeroArray fast path.
if (count == 0) {
return @[]; // Does not actually call +array when optimized.
}
// NSSingleObjectArray fast path. Retain/release here is worth it.
if (count == 1) {
NSArray *result = [[NSArray alloc] initWithObjects:pointers count:1];
pointers[0] = nil;
return result;
}
NSArray *result = (__bridge_transfer NSArray *)CFArrayCreate(gTransferAllocator, (const void **)(void *)pointers, count, &callbacks);
memset(pointers, 0, count * sizeof(id));
return result;
}
@end

View File

@ -0,0 +1,64 @@
//
// ASConfiguration.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
/// Not too performance-sensitive here.
@implementation ASConfiguration
- (instancetype)initWithDictionary:(NSDictionary *)dictionary
{
if (self = [super init]) {
if (dictionary != nil) {
const auto featureStrings = ASDynamicCast(dictionary[@"experimental_features"], NSArray);
const auto version = ASDynamicCast(dictionary[@"version"], NSNumber).integerValue;
if (version != ASConfigurationSchemaCurrentVersion) {
NSLog(@"Texture warning: configuration schema is old version (%ld vs %ld)", (long)version, (long)ASConfigurationSchemaCurrentVersion);
}
self.experimentalFeatures = ASExperimentalFeaturesFromArray(featureStrings);
} else {
self.experimentalFeatures = kNilOptions;
}
}
return self;
}
- (id)copyWithZone:(NSZone *)zone
{
ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil];
config.experimentalFeatures = self.experimentalFeatures;
config.delegate = self.delegate;
return config;
}
@end
//#define AS_FIXED_CONFIG_JSON "{ \"version\" : 1, \"experimental_features\": [ \"exp_text_node\" ] }"
#ifdef AS_FIXED_CONFIG_JSON
@implementation ASConfiguration (UserProvided)
+ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED
{
NSData *data = [@AS_FIXED_CONFIG_JSON dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (!d) {
NSAssert(NO, @"Error parsing fixed config string '%s': %@", AS_FIXED_CONFIG_JSON, error);
return nil;
} else {
return [[ASConfiguration alloc] initWithDictionary:d];
}
}
@end
#endif // AS_FIXED_CONFIG_JSON

View File

@ -0,0 +1,111 @@
//
// ASConfigurationInternal.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationDelegate.h>
#import <stdatomic.h>
static ASConfigurationManager *ASSharedConfigurationManager;
static dispatch_once_t ASSharedConfigurationManagerOnceToken;
NS_INLINE ASConfigurationManager *ASConfigurationManagerGet() {
dispatch_once(&ASSharedConfigurationManagerOnceToken, ^{
ASSharedConfigurationManager = [[ASConfigurationManager alloc] init];
});
return ASSharedConfigurationManager;
}
@implementation ASConfigurationManager {
ASConfiguration *_config;
dispatch_queue_t _delegateQueue;
BOOL _frameworkInitialized;
_Atomic(ASExperimentalFeatures) _activatedExperiments;
}
+ (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED
{
ASConfiguration *config = [[ASConfiguration alloc] init];
// TODO(wsdwsd0829): Fix #788 before enabling it.
// config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing;
return config;
}
- (instancetype)init
{
if (self = [super init]) {
_delegateQueue = dispatch_queue_create("org.TextureGroup.Texture.ConfigNotifyQueue", DISPATCH_QUEUE_SERIAL);
if ([ASConfiguration respondsToSelector:@selector(textureConfiguration)]) {
_config = [[ASConfiguration textureConfiguration] copy];
} else {
_config = [ASConfigurationManager defaultConfiguration];
}
}
return self;
}
- (void)frameworkDidInitialize
{
ASDisplayNodeAssertMainThread();
if (_frameworkInitialized) {
ASDisplayNodeFailAssert(@"Framework initialized twice.");
return;
}
_frameworkInitialized = YES;
const auto delegate = _config.delegate;
if ([delegate respondsToSelector:@selector(textureDidInitialize)]) {
[delegate textureDidInitialize];
}
}
- (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested
{
if (_config == nil) {
return NO;
}
NSAssert(__builtin_popcountl(requested) == 1, @"Cannot activate multiple features at once with this method.");
// We need to call out, whether it's enabled or not.
// A/B testing requires even "control" users to be activated.
ASExperimentalFeatures enabled = requested & _config.experimentalFeatures;
ASExperimentalFeatures prevTriggered = atomic_fetch_or(&_activatedExperiments, requested);
ASExperimentalFeatures newlyTriggered = requested & ~prevTriggered;
// Notify delegate if needed.
if (newlyTriggered != 0) {
__unsafe_unretained id<ASConfigurationDelegate> del = _config.delegate;
dispatch_async(_delegateQueue, ^{
[del textureDidActivateExperimentalFeatures:newlyTriggered];
});
}
return (enabled != 0);
}
// Define this even when !DEBUG, since we may run our tests in release mode.
+ (void)test_resetWithConfiguration:(ASConfiguration *)configuration
{
ASConfigurationManager *inst = ASConfigurationManagerGet();
inst->_config = configuration ?: [self defaultConfiguration];
atomic_store(&inst->_activatedExperiments, 0);
}
@end
BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures feature)
{
return [ASConfigurationManagerGet() activateExperimentalFeature:feature];
}
void ASNotifyInitialized()
{
[ASConfigurationManagerGet() frameworkDidInitialize];
}

View File

@ -0,0 +1,18 @@
//
// ASControlNode+Private.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlNode.h>
@interface ASControlNode (Private)
#if TARGET_OS_TV
- (void)_pressDown;
#endif
@end

View File

@ -0,0 +1,499 @@
//
// ASControlNode.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlNode.h>
#import "ASControlNode+Private.h"
#import <AsyncDisplayKit/ASControlNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASControlTargetAction.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASThread.h>
// UIControl allows dragging some distance outside of the control itself during
// tracking. This value depends on the device idiom (25 or 70 points), so
// so replicate that effect with the same values here for our own controls.
#define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f)
// Initial capacities for dispatch tables.
#define kASControlNodeEventDispatchTableInitialCapacity 4
#define kASControlNodeActionDispatchTableInitialCapacity 4
@interface ASControlNode ()
{
@private
// Control Attributes
BOOL _enabled;
BOOL _highlighted;
// Tracking
BOOL _tracking;
BOOL _touchInside;
// Target action pairs stored in an array for each event type
// ASControlEvent -> [ASTargetAction0, ASTargetAction1]
NSMutableDictionary<id<NSCopying>, NSMutableArray<ASControlTargetAction *> *> *_controlEventDispatchTable;
}
// Read-write overrides.
@property (getter=isTracking) BOOL tracking;
@property (getter=isTouchInside) BOOL touchInside;
/**
@abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event.
@param controlEvent A control event.
@result A key for use in _controlEventDispatchTable.
*/
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent);
/**
@abstract Enumerates the ASControlNode events included mask, invoking the block for each event.
@param mask An ASControlNodeEvent mask.
@param block The block to be invoked for each ASControlNodeEvent included in mask.
*/
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent));
/**
@abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking.
@param controlNode A control node.
@result The expanded bounds of the node.
*/
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode);
@end
@implementation ASControlNode
{
}
#pragma mark - Lifecycle
- (instancetype)init
{
if (!(self = [super init]))
return nil;
_enabled = YES;
// As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on.
self.userInteractionEnabled = NO;
return self;
}
#if TARGET_OS_TV
- (void)didLoad
{
[super didLoad];
// On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them.
// Here we add our own internal tap gesture to handle this behaviour.
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)];
tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)];
[self.view addGestureRecognizer:tapGestureRec];
}
#endif
- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled
{
[super setUserInteractionEnabled:userInteractionEnabled];
self.isAccessibilityElement = userInteractionEnabled;
}
- (void)__exitHierarchy
{
[super __exitHierarchy];
// If a control node is exit the hierarchy and is tracking we have to cancel it
if (self.tracking) {
[self _cancelTrackingWithEvent:nil];
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-missing-super-calls"
#pragma mark - ASDisplayNode Overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// Check if the tracking should start
UITouch *theTouch = [touches anyObject];
if (![self beginTrackingWithTouch:theTouch withEvent:event]) {
return;
}
// If we get more than one touch down on us, cancel.
// Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation.
if (touches.count > 1 || self.tracking) {
[self _cancelTrackingWithEvent:event];
} else {
// Otherwise, begin tracking.
self.tracking = YES;
// No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds.
self.touchInside = YES;
self.highlighted = YES;
// Send the appropriate touch-down control event depending on how many times we've been tapped.
ASControlNodeEvent controlEventMask = (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat;
[self sendActionsForControlEvents:controlEventMask withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
NSParameterAssert(touches.count == 1);
UITouch *theTouch = [touches anyObject];
// Check if tracking should continue
if (!self.tracking || ![self continueTrackingWithTouch:theTouch withEvent:event]) {
self.tracking = NO;
return;
}
CGPoint touchLocation = [theTouch locationInView:self.view];
// Update our touchInside state.
BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil];
// Update our highlighted state.
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
self.touchInside = dragIsInsideExpandedBounds;
self.highlighted = dragIsInsideExpandedBounds;
[self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside)
withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// Note that we've cancelled tracking.
[self _cancelTrackingWithEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
// If we're not interested in touches, we have nothing to do.
if (!self.enabled) {
return;
}
// On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent:
// twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to
// trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking
// state in order to have a correct behavior.
// It might be related to that issue: http://www.openradar.me/22910171
if (!self.tracking) {
return;
}
NSParameterAssert([touches count] == 1);
UITouch *theTouch = [touches anyObject];
CGPoint touchLocation = [theTouch locationInView:self.view];
// Update state.
self.tracking = NO;
self.touchInside = NO;
self.highlighted = NO;
// Note that we've ended tracking.
[self endTrackingWithTouch:theTouch withEvent:event];
// Send the appropriate touch-up control event.
CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self);
BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation);
[self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside)
withEvent:event];
}
- (void)_cancelTrackingWithEvent:(UIEvent *)event
{
// We're no longer tracking and there is no touch to be inside.
self.tracking = NO;
self.touchInside = NO;
self.highlighted = NO;
// Send the cancel event.
[self sendActionsForControlEvents:ASControlNodeEventTouchCancel withEvent:event];
}
#pragma clang diagnostic pop
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread();
// If not enabled we should not care about receving touches
if (! self.enabled) {
return nil;
}
return [super hitTest:point withEvent:event];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir.
if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) {
UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer;
// Allow double-tap gestures
return tapRecognizer.numberOfTapsRequired != 1;
}
// Otherwise, go ahead. :]
return YES;
}
- (BOOL)supportsLayerBacking
{
return super.supportsLayerBacking && !self.userInteractionEnabled;
}
#pragma mark - Action Messages
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
{
NSParameterAssert(action);
NSParameterAssert(controlEventMask != 0);
// ASControlNode cannot be layer backed if adding a target
ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair.");
ASLockScopeSelf();
if (!_controlEventDispatchTable) {
_controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries.
}
// Create new target action pair
ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init];
targetAction.action = action;
targetAction.target = target;
// Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent)
{
// Do we already have an event table for this control event?
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
if (!eventTargetActionArray) {
eventTargetActionArray = [[NSMutableArray alloc] init];
}
// Remove any prior target-action pair for this event, as UIKit does.
[eventTargetActionArray removeObject:targetAction];
// Register the new target-action as the last one to be sent.
[eventTargetActionArray addObject:targetAction];
if (eventKey) {
[_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey];
}
});
self.userInteractionEnabled = YES;
}
- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent
{
NSParameterAssert(target);
NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents);
ASLockScopeSelf();
// Grab the event target action array for this event.
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)];
if (!eventTargetActionArray) {
return nil;
}
NSMutableArray *actions = [[NSMutableArray alloc] init];
// Collect all actions for this target.
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) {
[actions addObject:NSStringFromSelector(targetAction.action)];
}
}
return actions;
}
- (NSSet *)allTargets
{
ASLockScopeSelf();
NSMutableSet *targets = [[NSMutableSet alloc] init];
// Look at each event...
for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) {
// and each event's targets...
for (ASControlTargetAction *targetAction in eventTargetActionArray) {
[targets addObject:targetAction.target];
}
}
return targets;
}
- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask
{
NSParameterAssert(controlEventMask != 0);
ASLockScopeSelf();
// Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^
(ASControlNodeEvent controlEvent)
{
// Grab the dispatch table for this event (if we have it).
id<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
if (!eventTargetActionArray) {
return;
}
NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
if (!target || evaluatedObject.target == target) {
if (!action) {
return NO;
} else if (evaluatedObject.action == action) {
return NO;
}
}
return YES;
}];
[eventTargetActionArray filterUsingPredicate:filterPredicate];
if (eventTargetActionArray.count == 0) {
// If there are no targets for this event anymore, remove it.
[_controlEventDispatchTable removeObjectForKey:eventKey];
}
});
}
#pragma mark -
- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event
{
ASDisplayNodeAssertMainThread(); //We access self.view below, it's not safe to call this off of main.
NSParameterAssert(controlEvents != 0);
NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray<ASControlTargetAction *> alloc] init];
{
ASLockScopeSelf();
// Enumerate the events in the mask, invoking the target-action pairs for each.
_ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^
(ASControlNodeEvent controlEvent)
{
// Iterate on each target action pair
for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) {
ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init];
resolvedTargetAction.action = targetAction.action;
resolvedTargetAction.target = targetAction.target;
// NSNull means that a nil target was set, so start at self and travel the responder chain
if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) {
// if the target cannot perform the action, travel the responder chain to try to find something that does
resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self];
}
if (resolvedTargetAction.target) {
[resolvedEventTargetActionArray addObject:resolvedTargetAction];
}
}
});
}
//We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
for (ASControlTargetAction *targetAction in resolvedEventTargetActionArray) {
[targetAction.target performSelector:targetAction.action withObject:self withObject:event];
}
#pragma clang diagnostic pop
}
#pragma mark - Convenience
id<NSCopying> _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent)
{
return @(controlEvent);
}
void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent))
{
if (block == nil) {
return;
}
// Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered)
for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) {
// If it's included in the mask, invoke the block.
if ((mask & thisEvent) == thisEvent)
block(thisEvent);
}
}
CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) {
return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset);
}
#pragma mark - For Subclasses
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
return YES;
}
- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent
{
// Subclass hook
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent
{
// Subclass hook
}
#pragma mark - Debug
- (ASDisplayNode *)debugHighlightOverlay
{
return nil;
}
@end

View File

@ -0,0 +1,65 @@
//
// ASControlTargetAction.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASControlTargetAction.h>
@implementation ASControlTargetAction
{
__weak id _target;
BOOL _createdWithNoTarget;
}
- (void)setTarget:(id)target {
_target = target;
if (!target) {
_createdWithNoTarget = YES;
}
}
- (id)target {
return _target;
}
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[ASControlTargetAction class]]) {
return NO;
}
ASControlTargetAction *otherObject = (ASControlTargetAction *)object;
BOOL areTargetsEqual;
if (self.target != nil && otherObject.target != nil && self.target == otherObject.target) {
areTargetsEqual = YES;
}
else if (self.target == nil && otherObject.target == nil && self.createdWithNoTarget && otherObject.createdWithNoTarget) {
areTargetsEqual = YES;
}
else {
areTargetsEqual = NO;
}
if (!areTargetsEqual) {
return NO;
}
if (self.action && otherObject.action && self.action == otherObject.action) {
return YES;
}
else {
return NO;
}
}
- (NSUInteger)hash {
return [self.target hash];
}
@end

View File

@ -0,0 +1,125 @@
//
// ASDimension.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
#import <AsyncDisplayKit/ASAssert.h>
#pragma mark - ASDimension
ASDimension const ASDimensionAuto = {ASDimensionUnitAuto, 0};
ASOVERLOADABLE ASDimension ASDimensionMake(NSString *dimension)
{
if (dimension.length > 0) {
// Handle points
if ([dimension hasSuffix:@"pt"]) {
return ASDimensionMake(ASDimensionUnitPoints, ASCGFloatFromString(dimension));
}
// Handle auto
if ([dimension isEqualToString:@"auto"]) {
return ASDimensionAuto;
}
// Handle percent
if ([dimension hasSuffix:@"%"]) {
return ASDimensionMake(ASDimensionUnitFraction, (ASCGFloatFromString(dimension) / 100.0));
}
}
return ASDimensionAuto;
}
NSString *NSStringFromASDimension(ASDimension dimension)
{
switch (dimension.unit) {
case ASDimensionUnitPoints:
return [NSString stringWithFormat:@"%.0fpt", dimension.value];
case ASDimensionUnitFraction:
return [NSString stringWithFormat:@"%.0f%%", dimension.value * 100.0];
case ASDimensionUnitAuto:
return @"Auto";
}
}
#pragma mark - ASLayoutSize
ASLayoutSize const ASLayoutSizeAuto = {ASDimensionAuto, ASDimensionAuto};
#pragma mark - ASSizeRange
ASSizeRange const ASSizeRangeZero = {};
ASSizeRange const ASSizeRangeUnconstrained = { {0, 0}, { INFINITY, INFINITY }};
struct _Range {
CGFloat min;
CGFloat max;
/**
Intersects another dimension range. If the other range does not overlap, this size range "wins" by returning a
single point within its own range that is closest to the non-overlapping range.
*/
_Range intersect(const _Range &other) const
{
CGFloat newMin = MAX(min, other.min);
CGFloat newMax = MIN(max, other.max);
if (newMin <= newMax) {
return {newMin, newMax};
} else {
// No intersection. If we're before the other range, return our max; otherwise our min.
if (min < other.min) {
return {max, max};
} else {
return {min, min};
}
}
}
};
ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange)
{
const auto w = _Range({sizeRange.min.width, sizeRange.max.width}).intersect({otherSizeRange.min.width, otherSizeRange.max.width});
const auto h = _Range({sizeRange.min.height, sizeRange.max.height}).intersect({otherSizeRange.min.height, otherSizeRange.max.height});
return {{w.min, h.min}, {w.max, h.max}};
}
NSString *NSStringFromASSizeRange(ASSizeRange sizeRange)
{
// 17 field length copied from iOS 10.3 impl of NSStringFromCGSize.
if (CGSizeEqualToSize(sizeRange.min, sizeRange.max)) {
return [NSString stringWithFormat:@"{{%.*g, %.*g}}",
17, sizeRange.min.width,
17, sizeRange.min.height];
}
return [NSString stringWithFormat:@"{{%.*g, %.*g}, {%.*g, %.*g}}",
17, sizeRange.min.width,
17, sizeRange.min.height,
17, sizeRange.max.width,
17, sizeRange.max.height];
}
#if YOGA
#pragma mark - Yoga - ASEdgeInsets
ASEdgeInsets const ASEdgeInsetsZero = {};
ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets)
{
ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero;
asEdgeInsets.top = ASDimensionMake(edgeInsets.top);
asEdgeInsets.left = ASDimensionMake(edgeInsets.left);
asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom);
asEdgeInsets.right = ASDimensionMake(edgeInsets.right);
return asEdgeInsets;
}
#endif

View File

@ -0,0 +1,65 @@
//
// ASDimensionInternal.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDimensionInternal.h>
#pragma mark - ASLayoutElementSize
NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size)
{
return [NSString stringWithFormat:
@"<ASLayoutElementSize: exact=%@, min=%@, max=%@>",
NSStringFromASLayoutSize(ASLayoutSizeMake(size.width, size.height)),
NSStringFromASLayoutSize(ASLayoutSizeMake(size.minWidth, size.minHeight)),
NSStringFromASLayoutSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight))];
}
ASDISPLAYNODE_INLINE void ASLayoutElementSizeConstrain(CGFloat minVal, CGFloat exactVal, CGFloat maxVal, CGFloat *outMin, CGFloat *outMax)
{
NSCAssert(!isnan(minVal), @"minVal must not be NaN");
NSCAssert(!isnan(maxVal), @"maxVal must not be NaN");
// Avoid use of min/max primitives since they're harder to reason
// about in the presence of NaN (in exactVal)
// Follow CSS: min overrides max overrides exact.
// Begin with the min/max range
*outMin = minVal;
*outMax = maxVal;
if (maxVal <= minVal) {
// min overrides max and exactVal is irrelevant
*outMax = minVal;
return;
}
if (isnan(exactVal)) {
// no exact value, so leave as a min/max range
return;
}
if (exactVal > maxVal) {
// clip to max value
*outMin = maxVal;
} else if (exactVal < minVal) {
// clip to min value
*outMax = minVal;
} else {
// use exact value
*outMin = *outMax = exactVal;
}
}
ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange)
{
CGSize resolvedExact = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.width, size.height), parentSize, {NAN, NAN});
CGSize resolvedMin = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.minWidth, size.minHeight), parentSize, autoASSizeRange.min);
CGSize resolvedMax = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight), parentSize, autoASSizeRange.max);
CGSize rangeMin, rangeMax;
ASLayoutElementSizeConstrain(resolvedMin.width, resolvedExact.width, resolvedMax.width, &rangeMin.width, &rangeMax.width);
ASLayoutElementSizeConstrain(resolvedMin.height, resolvedExact.height, resolvedMax.height, &rangeMin.height, &rangeMax.height);
return {rangeMin, rangeMax};
}

View File

@ -0,0 +1,27 @@
//
// ASDispatch.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
AS_EXTERN void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));
/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
AS_EXTERN void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i));

View File

@ -0,0 +1,63 @@
//
// ASDispatch.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASDispatch.h"
#import <AsyncDisplayKit/ASConfigurationInternal.h>
// Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well.
#import <stdatomic.h>
/**
* Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
if (ASActivateExperimentalFeature(ASExperimentalDispatchApply)) {
dispatch_apply(iterationCount, queue, work);
return;
}
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
dispatch_group_t group = dispatch_group_create();
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_group_async(group, queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
};
/**
* Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs.
*
* Note: The actual number of threads may be lower than threadCount, if libdispatch
* decides the system can't handle it. In reality this rarely happens.
*/
void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) {
if (threadCount == 0) {
threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2;
}
__block atomic_size_t counter = ATOMIC_VAR_INIT(0);
for (NSUInteger t = 0; t < threadCount; t++) {
dispatch_async(queue, ^{
size_t i;
while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) {
work(i);
}
});
}
};

View File

@ -0,0 +1,90 @@
//
// ASDisplayNode+Ancestry.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode+Ancestry.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
AS_SUBCLASSING_RESTRICTED
@interface ASNodeAncestryEnumerator : NSEnumerator
@end
@implementation ASNodeAncestryEnumerator {
ASDisplayNode *_lastNode; // This needs to be strong because enumeration will not retain the current batch of objects
BOOL _initialState;
}
- (instancetype)initWithNode:(ASDisplayNode *)node
{
if (self = [super init]) {
_initialState = YES;
_lastNode = node;
}
return self;
}
- (id)nextObject
{
if (_initialState) {
_initialState = NO;
return _lastNode;
}
ASDisplayNode *nextNode = _lastNode.supernode;
if (nextNode == nil && ASDisplayNodeThreadIsMain()) {
CALayer *layer = _lastNode.nodeLoaded ? _lastNode.layer.superlayer : nil;
while (layer != nil) {
nextNode = ASLayerToDisplayNode(layer);
if (nextNode != nil) {
break;
}
layer = layer.superlayer;
}
}
_lastNode = nextNode;
return nextNode;
}
@end
@implementation ASDisplayNode (Ancestry)
- (id<NSFastEnumeration>)supernodes
{
NSEnumerator *result = [[ASNodeAncestryEnumerator alloc] initWithNode:self];
[result nextObject]; // discard first object (self)
return result;
}
- (id<NSFastEnumeration>)supernodesIncludingSelf
{
return [[ASNodeAncestryEnumerator alloc] initWithNode:self];
}
- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf
{
id<NSFastEnumeration> chain = includeSelf ? self.supernodesIncludingSelf : self.supernodes;
for (ASDisplayNode *ancestor in chain) {
if ([ancestor isKindOfClass:supernodeClass]) {
return ancestor;
}
}
return nil;
}
- (NSString *)ancestryDescription
{
NSMutableArray *strings = [NSMutableArray array];
for (ASDisplayNode *node in self.supernodes) {
[strings addObject:ASObjectDescriptionMakeTiny(node)];
}
return strings.description;
}
@end

View File

@ -0,0 +1,493 @@
//
// ASDisplayNode+AsyncDisplay.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import "ASSignpost.h"
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
using AS::MutexLocker;
@interface ASDisplayNode () <_ASDisplayLayerDelegate>
@end
@implementation ASDisplayNode (AsyncDisplay)
#if ASDISPLAYNODE_DELAY_DISPLAY
#define ASDN_DELAY_FOR_DISPLAY() usleep( (long)(0.1 * USEC_PER_SEC) )
#else
#define ASDN_DELAY_FOR_DISPLAY()
#endif
#define CHECK_CANCELLED_AND_RETURN_NIL(expr) if (isCancelledBlock()) { \
expr; \
return nil; \
} \
- (NSObject *)drawParameters
{
__instanceLock__.lock();
BOOL implementsDrawParameters = _flags.implementsDrawParameters;
__instanceLock__.unlock();
if (implementsDrawParameters) {
return [self drawParametersForAsyncLayer:self.asyncLayer];
} else {
return nil;
}
}
- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks
{
// Skip subtrees that are hidden or zero alpha.
if (self.isHidden || self.alpha <= 0.0) {
return;
}
__instanceLock__.lock();
BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized);
__instanceLock__.unlock();
// if super node is rasterizing descendants, subnodes will not have had layout calls because they don't have layers
if (rasterizingFromAscendent) {
[self __layout];
}
// Capture these outside the display block so they are retained.
UIColor *backgroundColor = self.backgroundColor;
CGRect bounds = self.bounds;
CGFloat cornerRadius = self.cornerRadius;
BOOL clipsToBounds = self.clipsToBounds;
CGRect frame;
// If this is the root container node, use a frame with a zero origin to draw into. If not, calculate the correct frame using the node's position, transform and anchorPoint.
if (self.rasterizesSubtree) {
frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height);
} else {
CGPoint position = self.position;
CGPoint anchorPoint = self.anchorPoint;
// Pretty hacky since full 3D transforms aren't actually supported, but attempt to compute the transformed frame of this node so that we can composite it into approximately the right spot.
CGAffineTransform transform = CATransform3DGetAffineTransform(self.transform);
CGSize scaledBoundsSize = CGSizeApplyAffineTransform(bounds.size, transform);
CGPoint origin = CGPointMake(position.x - scaledBoundsSize.width * anchorPoint.x,
position.y - scaledBoundsSize.height * anchorPoint.y);
frame = CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height);
}
// Get the display block for this node.
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES];
// We'll display something if there is a display block, clipping, translation and/or a background color.
BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds;
// If we should display, then push a transform, draw the background color, and draw the contents.
// The transform is popped in a block added after the recursion into subnodes.
if (shouldDisplay) {
dispatch_block_t pushAndDisplayBlock = ^{
// Push transform relative to parent.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, frame.origin.x, frame.origin.y);
//support cornerRadius
if (rasterizingFromAscendent && clipsToBounds) {
if (cornerRadius) {
[[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip];
} else {
CGContextClipToRect(context, bounds);
}
}
// Fill background if any.
CGColorRef backgroundCGColor = backgroundColor.CGColor;
if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) {
CGContextSetFillColorWithColor(context, backgroundCGColor);
CGContextFillRect(context, bounds);
}
// If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store).
if (displayBlock) {
UIImage *image = (UIImage *)displayBlock();
if (image) {
BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage));
CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal;
[image drawInRect:bounds blendMode:blendMode alpha:1];
}
}
};
[displayBlocks addObject:pushAndDisplayBlock];
}
// Recursively capture displayBlocks for all descendants.
for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
}
// If we pushed a transform, pop it by adding a display block that does nothing other than that.
if (shouldDisplay) {
// Since this block is pure, we can store it statically.
static dispatch_block_t popBlock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
popBlock = ^{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextRestoreGState(context);
};
});
[displayBlocks addObject:popBlock];
}
}
- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous
isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock
rasterizing:(BOOL)rasterizing
{
ASDisplayNodeAssertMainThread();
asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil;
ASDisplayNodeFlags flags;
__instanceLock__.lock();
flags = _flags;
// We always create a graphics context, unless a -display method is used, OR if we are a subnode drawing into a rasterized parent.
BOOL shouldCreateGraphicsContext = (flags.implementsImageDisplay == NO && rasterizing == NO);
BOOL shouldBeginRasterizing = (rasterizing == NO && flags.rasterizesSubtree);
BOOL usesImageDisplay = flags.implementsImageDisplay;
BOOL usesDrawRect = flags.implementsDrawRect;
if (usesImageDisplay == NO && usesDrawRect == NO && shouldBeginRasterizing == NO) {
// Early exit before requesting more expensive properties like bounds and opaque from the layer.
__instanceLock__.unlock();
return nil;
}
BOOL opaque = self.opaque;
CGRect bounds = self.bounds;
UIColor *backgroundColor = self.backgroundColor;
CGColorRef borderColor = self.borderColor;
CGFloat borderWidth = self.borderWidth;
CGFloat contentsScaleForDisplay = _contentsScaleForDisplay;
__instanceLock__.unlock();
// Capture drawParameters from delegate on main thread, if this node is displaying itself rather than recursively rasterizing.
id drawParameters = (shouldBeginRasterizing == NO ? [self drawParameters] : nil);
// Only the -display methods should be called if we can't size the graphics buffer to use.
if (CGRectIsEmpty(bounds) && (shouldBeginRasterizing || shouldCreateGraphicsContext)) {
return nil;
}
ASDisplayNodeAssert(contentsScaleForDisplay != 0.0, @"Invalid contents scale");
ASDisplayNodeAssert(rasterizing || !(_hierarchyState & ASHierarchyStateRasterized),
@"Rasterized descendants should never display unless being drawn into the rasterized container.");
if (shouldBeginRasterizing) {
// Collect displayBlocks for all descendants.
NSMutableArray *displayBlocks = [[NSMutableArray alloc] init];
[self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks];
CHECK_CANCELLED_AND_RETURN_NIL();
// If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing.
// Unlike CALayer drawing, we include the backgroundColor as a base during rasterization.
opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f;
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
for (dispatch_block_t block in displayBlocks) {
CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext());
block();
}
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
ASDN_DELAY_FOR_DISPLAY();
return image;
};
} else {
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
if (shouldCreateGraphicsContext) {
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
}
CGContextRef currentContext = UIGraphicsGetCurrentContext();
UIImage *image = nil;
if (shouldCreateGraphicsContext && !currentContext) {
//ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return nil;
}
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
}
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
if (shouldCreateGraphicsContext) {
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
image = ASGraphicsGetImageAndEndCurrentContext();
}
ASDN_DELAY_FOR_DISPLAY();
return image;
};
}
/**
If we're profiling, wrap the display block with signpost start and end.
Color the interval red if cancelled, green otherwise.
*/
#if AS_KDEBUG_ENABLE
__unsafe_unretained id ptrSelf = self;
displayBlock = ^{
ASSignpostStartCustom(ASSignpostLayerDisplay, ptrSelf, 0);
id result = displayBlock();
ASSignpostEndCustom(ASSignpostLayerDisplay, ptrSelf, 0, isCancelledBlock() ? ASSignpostColorRed : ASSignpostColorGreen);
return result;
};
#endif
return displayBlock;
}
- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters
{
if (context) {
__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) {
ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
// TODO: This clip path should be removed if we are rasterizing.
CGRect boundingBox = CGContextGetClipBoundingBox(context);
[[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip];
}
if (willDisplayNodeContentWithRenderingContext) {
willDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
}
- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor
{
if (context == NULL && *image == NULL) {
return;
}
__instanceLock__.lock();
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
CGFloat cornerRadius = _cornerRadius;
CGFloat contentsScale = _contentsScaleForDisplay;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
if (context != NULL) {
if (didDisplayNodeContentWithRenderingContext) {
didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) {
CGRect bounds = CGRectZero;
if (context == NULL) {
bounds = self.threadSafeBounds;
bounds.size.width *= contentsScale;
bounds.size.height *= contentsScale;
CGFloat white = 0.0f, alpha = 0.0f;
[backgroundColor getWhite:&white alpha:&alpha];
ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale);
[*image drawInRect:bounds];
} else {
bounds = CGContextGetClipBoundingBox(context);
}
ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds];
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]];
roundedHole.usesEvenOddFillRule = YES;
UIBezierPath *roundedPath = nil;
if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0
CGFloat strokeThickness = borderWidth * contentsScale;
CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f;
roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset)
cornerRadius:_cornerRadius * contentsScale];
roundedPath.lineWidth = strokeThickness;
[[UIColor colorWithCGColor:borderColor] setStroke];
}
// Punch out the corners by copying the backgroundColor over them.
// This works for everything from clearColor to opaque colors.
[backgroundColor setFill];
[roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f];
[roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil.
if (*image) {
*image = ASGraphicsGetImageAndEndCurrentContext();
}
}
}
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
{
ASDisplayNodeAssertMainThread();
__instanceLock__.lock();
if (_hierarchyState & ASHierarchyStateRasterized) {
__instanceLock__.unlock();
return;
}
CALayer *layer = _layer;
BOOL rasterizesSubtree = _flags.rasterizesSubtree;
__instanceLock__.unlock();
// for async display, capture the current displaySentinel value to bail early when the job is executed if another is
// enqueued
// for sync display, do not support cancellation
// FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing
// from the displayQueue? Need to not cancel early fails from displaySentinel changes.
asdisplaynode_iscancelled_block_t isCancelledBlock = nil;
if (asynchronously) {
uint displaySentinelValue = ++_displaySentinel;
__weak ASDisplayNode *weakSelf = self;
isCancelledBlock = ^BOOL{
__strong ASDisplayNode *self = weakSelf;
return self == nil || (displaySentinelValue != self->_displaySentinel.load());
};
} else {
isCancelledBlock = ^BOOL{
return NO;
};
}
// Set up displayBlock to call either display or draw on the delegate and return a UIImage contents
asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO];
if (!displayBlock) {
return;
}
ASDisplayNodeAssert(layer, @"Expect _layer to be not nil");
// This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously
asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id<NSObject> value, BOOL canceled){
ASDisplayNodeCAssertMainThread();
if (!canceled && !isCancelledBlock()) {
UIImage *image = (UIImage *)value;
BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero));
if (stretchable) {
ASDisplayNodeSetResizableContents(layer, image);
} else {
layer.contentsScale = self.contentsScale;
layer.contents = (id)image.CGImage;
}
[self didDisplayAsyncLayer:self.asyncLayer];
if (rasterizesSubtree) {
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
[node didDisplayAsyncLayer:node.asyncLayer];
});
}
}
};
// Call willDisplay immediately in either case
[self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously];
if (rasterizesSubtree) {
ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) {
[node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously];
});
}
if (asynchronously) {
// Async rendering operations are contained by a transaction, which allows them to proceed and concurrently
// while synchronizing the final application of the results to the layer's contents property (completionBlock).
// First, look to see if we are expected to join a parent's transaction container.
CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer;
// In the case that a transaction does not yet exist (such as for an individual node outside of a container),
// this call will allocate the transaction and add it to _ASAsyncTransactionGroup.
// It will automatically commit the transaction at the end of the runloop.
_ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction;
// Adding this displayBlock operation to the transaction will start it IMMEDIATELY.
// The only function of the transaction commit is to gate the calling of the completionBlock.
[transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock];
} else {
UIImage *contents = (UIImage *)displayBlock();
completionBlock(contents, NO);
}
}
- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer
{
_displaySentinel.fetch_add(1);
}
- (ASDisplayNodeContextModifier)willDisplayNodeContentWithRenderingContext
{
MutexLocker l(__instanceLock__);
return _willDisplayNodeContentWithRenderingContext;
}
- (ASDisplayNodeContextModifier)didDisplayNodeContentWithRenderingContext
{
MutexLocker l(__instanceLock__);
return _didDisplayNodeContentWithRenderingContext;
}
- (void)setWillDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier
{
MutexLocker l(__instanceLock__);
_willDisplayNodeContentWithRenderingContext = contextModifier;
}
- (void)setDidDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier;
{
MutexLocker l(__instanceLock__);
_didDisplayNodeContentWithRenderingContext = contextModifier;
}
@end

View File

@ -0,0 +1,40 @@
//
// ASDisplayNode+Convenience.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <UIKit/UIViewController.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import "ASResponderChainEnumerator.h"
@implementation ASDisplayNode (Convenience)
- (__kindof UIViewController *)closestViewController
{
ASDisplayNodeAssertMainThread();
// Careful not to trigger node loading here.
if (!self.nodeLoaded) {
return nil;
}
// Get the closest view.
UIView *view = ASFindClosestViewOfLayer(self.layer);
// Travel up the responder chain to find a view controller.
for (UIResponder *responder in [view asdk_responderChainEnumerator]) {
UIViewController *vc = ASDynamicCast(responder, UIViewController);
if (vc != nil) {
return vc;
}
}
return nil;
}
@end

View File

@ -0,0 +1,142 @@
//
// ASDisplayNode+Deprecated.h
// Texture
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#pragma once
#import <AsyncDisplayKit/ASDisplayNode.h>
@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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
//
// ASDisplayNode+LayoutSpec.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import "_ASScopeTimer.h"
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASLayout.h>
#import "ASLayoutSpec+Subclasses.h"
#import "ASLayoutSpecPrivate.h"
#import <AsyncDisplayKit/ASThread.h>
@implementation ASDisplayNode (ASLayoutSpec)
- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock
{
// For now there should never be an override of layoutSpecThatFits: and a layoutSpecBlock together.
ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits),
@"Nodes with a .layoutSpecBlock must not also implement -layoutSpecThatFits:");
AS::MutexLocker l(__instanceLock__);
_layoutSpecBlock = layoutSpecBlock;
}
- (ASLayoutSpecBlock)layoutSpecBlock
{
AS::MutexLocker l(__instanceLock__);
return _layoutSpecBlock;
}
- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize
{
AS::UniqueLock l(__instanceLock__);
// Manual size calculation via calculateSizeThatFits:
if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) {
CGSize size = [self calculateSizeThatFits:constrainedSize.max];
ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size));
return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil];
}
// Size calcualtion with layout elements
BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec;
if (measureLayoutSpec) {
_layoutSpecNumberOfPasses++;
}
// Get layout element from the node
id<ASLayoutElement> layoutElement = [self _locked_layoutElementThatFits:constrainedSize];
#if ASEnableVerboseLogging
for (NSString *asciiLine in [[layoutElement asciiArtString] componentsSeparatedByString:@"\n"]) {
as_log_verbose(ASLayoutLog(), "%@", asciiLine);
}
#endif
// Certain properties are necessary to set on an element of type ASLayoutSpec
if (layoutElement.layoutElementType == ASLayoutElementTypeLayoutSpec) {
ASLayoutSpec *layoutSpec = (ASLayoutSpec *)layoutElement;
#if AS_DEDUPE_LAYOUT_SPEC_TREE
NSHashTable *duplicateElements = [layoutSpec findDuplicatedElementsInSubtree];
if (duplicateElements.count > 0) {
ASDisplayNodeFailAssert(@"Node %@ returned a layout spec that contains the same elements in multiple positions. Elements: %@", self, duplicateElements);
// Use an empty layout spec to avoid crashes
layoutSpec = [[ASLayoutSpec alloc] init];
}
#endif
ASDisplayNodeAssert(layoutSpec.isMutable, @"Node %@ returned layout spec %@ that has already been used. Layout specs should always be regenerated.", self, layoutSpec);
layoutSpec.isMutable = NO;
}
// Manually propagate the trait collection here so that any layoutSpec children of layoutSpec will get a traitCollection
{
AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec);
ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection);
}
BOOL measureLayoutComputation = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation;
if (measureLayoutComputation) {
_layoutComputationNumberOfPasses++;
}
// Layout element layout creation
ASLayout *layout = ({
AS::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation);
[layoutElement layoutThatFits:constrainedSize];
});
ASDisplayNodeAssertNotNil(layout, @"[ASLayoutElement layoutThatFits:] should never return nil! %@, %@", self, layout);
// Make sure layoutElementObject of the root layout is `self`, so that the flattened layout will be structurally correct.
BOOL isFinalLayoutElement = (layout.layoutElement != self);
if (isFinalLayoutElement) {
layout.position = CGPointZero;
layout = [ASLayout layoutWithLayoutElement:self size:layout.size sublayouts:@[layout]];
}
ASDisplayNodeLogEvent(self, @"computedLayout: %@", layout);
// PR #1157: Reduces accuracy of _unflattenedLayout for debugging/Weaver
if ([ASDisplayNode shouldStoreUnflattenedLayouts]) {
_unflattenedLayout = layout;
}
layout = [layout filteredNodeLayoutTree];
return layout;
}
- (id<ASLayoutElement>)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize
{
ASAssertLocked(__instanceLock__);
BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec;
if (_layoutSpecBlock != NULL) {
return ({
AS::MutexLocker l(__instanceLock__);
AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec);
_layoutSpecBlock(self, constrainedSize);
});
} else {
return ({
AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec);
[self layoutSpecThatFits:constrainedSize];
});
}
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
__ASDisplayNodeCheckForLayoutMethodOverrides;
ASDisplayNodeAssert(NO, @"-[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported.");
return [[ASLayoutSpec alloc] init];
}
@end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,339 @@
//
// ASDisplayNodeExtras.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Ancestry.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <queue>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr) {
/**
* UIKit components must be deallocated on the main thread. We use this shared
* run loop queue to gradually deallocate them across many turns of the main run loop.
*/
static ASRunLoopQueue *queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil];
queue.batchSize = 10;
});
if (objectPtr != NULL && *objectPtr != nil) {
// TODO: If ASRunLoopQueue supported an "unsafe_unretained" mode, we could
// transfer the caller's +1 into it and save the retain/release pair.
// Lock queue while enqueuing and releasing, so that there's no risk
// that the queue will release before we get a chance to release.
[queue lock];
[queue enqueue:*objectPtr]; // Retain, +1
*objectPtr = nil; // Release, +0
[queue unlock]; // (After queue drains), release, -1
}
}
void _ASSetDebugNames(Class _Nonnull owningClass, NSString * _Nonnull names, ASDisplayNode * _Nullable object, ...)
{
NSString *owningClassName = NSStringFromClass(owningClass);
NSArray *nameArray = [names componentsSeparatedByString:@", "];
va_list args;
va_start(args, object);
NSInteger i = 0;
for (ASDisplayNode *node = object; node != nil; node = va_arg(args, id), i++) {
NSMutableString *symbolName = [nameArray[i] mutableCopy];
// Remove any `self.` or `_` prefix
[symbolName replaceOccurrencesOfString:@"self." withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)];
[symbolName replaceOccurrencesOfString:@"_" withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)];
node.debugName = [NSString stringWithFormat:@"%@.%@", owningClassName, symbolName];
}
ASDisplayNodeCAssert(nameArray.count == i, @"Malformed call to ASSetDebugNames: %@", names);
va_end(args);
}
ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window)
{
ASDisplayNodeCAssert(![displayNode isLayerBacked], @"displayNode must not be layer backed as it may have a nil window");
if (displayNode && [displayNode supportsRangeManagedInterfaceState]) {
// Directly clear the visible bit if we are not in a window. This means that the interface state is,
// if not already, about to be set to invisible as it is not possible for an element to be visible
// while outside of a window.
ASInterfaceState interfaceState = displayNode.pendingInterfaceState;
return (window == nil ? (interfaceState &= (~ASInterfaceStateVisible)) : interfaceState);
} else {
// For not range managed nodes we might be on our own to try to guess if we're visible.
return (window == nil ? ASInterfaceStateNone : (ASInterfaceStateVisible | ASInterfaceStateDisplay));
}
}
ASDisplayNode *ASLayerToDisplayNode(CALayer *layer)
{
return layer.asyncdisplaykit_node;
}
ASDisplayNode *ASViewToDisplayNode(UIView *view)
{
return view.asyncdisplaykit_node;
}
void ASDisplayNodePerformBlockOnEveryNode(CALayer * _Nullable layer, ASDisplayNode * _Nullable node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node))
{
if (!node) {
ASDisplayNodeCAssertNotNil(layer, @"Cannot recursively perform with nil node and nil layer");
ASDisplayNodeCAssertMainThread();
node = ASLayerToDisplayNode(layer);
}
if (node) {
block(node);
}
if (traverseSublayers && !layer && [node isNodeLoaded] && ASDisplayNodeThreadIsMain()) {
layer = node.layer;
}
if (traverseSublayers && layer && node.rasterizesSubtree == NO) {
/// NOTE: The docs say `sublayers` returns a copy, but it does not.
/// See: http://stackoverflow.com/questions/14854480/collection-calayerarray-0x1ed8faa0-was-mutated-while-being-enumerated
for (CALayer *sublayer in [[layer sublayers] copy]) {
ASDisplayNodePerformBlockOnEveryNode(sublayer, nil, traverseSublayers, block);
}
} else if (node) {
for (ASDisplayNode *subnode in [node subnodes]) {
ASDisplayNodePerformBlockOnEveryNode(nil, subnode, traverseSublayers, block);
}
}
}
void ASDisplayNodePerformBlockOnEveryNodeBFS(ASDisplayNode *node, void(^block)(ASDisplayNode *node))
{
// Queue used to keep track of subnodes while traversing this layout in a BFS fashion.
std::queue<ASDisplayNode *> queue;
queue.push(node);
while (!queue.empty()) {
node = queue.front();
queue.pop();
block(node);
// Add all subnodes to process in next step
for (ASDisplayNode *subnode in node.subnodes) {
queue.push(subnode);
}
}
}
void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node))
{
for (ASDisplayNode *subnode in node.subnodes) {
ASDisplayNodePerformBlockOnEveryNode(nil, subnode, YES, block);
}
}
ASDisplayNode *ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node))
{
// This function has historically started with `self` but the name suggests
// that it wouldn't. Perhaps we should change the behavior.
for (ASDisplayNode *ancestor in node.supernodesIncludingSelf) {
if (block(ancestor)) {
return ancestor;
}
}
return nil;
}
__kindof ASDisplayNode *ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c)
{
// This function has historically started with `self` but the name suggests
// that it wouldn't. Perhaps we should change the behavior.
return [start supernodeOfClass:c includingSelf:YES];
}
static void _ASCollectDisplayNodes(NSMutableArray *array, CALayer *layer)
{
ASDisplayNode *node = ASLayerToDisplayNode(layer);
if (nil != node) {
[array addObject:node];
}
for (CALayer *sublayer in layer.sublayers)
_ASCollectDisplayNodes(array, sublayer);
}
NSArray<ASDisplayNode *> *ASCollectDisplayNodes(ASDisplayNode *node)
{
NSMutableArray *list = [[NSMutableArray alloc] init];
for (CALayer *sublayer in node.layer.sublayers) {
_ASCollectDisplayNodes(list, sublayer);
}
return list;
}
#pragma mark - Find all subnodes
static void _ASDisplayNodeFindAllSubnodes(NSMutableArray *array, ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node))
{
if (!node)
return;
for (ASDisplayNode *subnode in node.subnodes) {
if (block(subnode)) {
[array addObject:subnode];
}
_ASDisplayNodeFindAllSubnodes(array, subnode, block);
}
}
NSArray<ASDisplayNode *> *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node))
{
NSMutableArray *list = [[NSMutableArray alloc] init];
_ASDisplayNodeFindAllSubnodes(list, start, block);
return list;
}
NSArray<__kindof ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c)
{
return ASDisplayNodeFindAllSubnodes(start, ^(ASDisplayNode *n) {
return [n isKindOfClass:c];
});
}
#pragma mark - Find first subnode
static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node))
{
for (ASDisplayNode *subnode in startNode.subnodes) {
ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block);
if (foundNode) {
return foundNode;
}
}
if (includeStartNode && block(startNode))
return startNode;
return nil;
}
__kindof ASDisplayNode *ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node))
{
return _ASDisplayNodeFindFirstNode(startNode, YES, block);
}
__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node))
{
return _ASDisplayNodeFindFirstNode(startNode, NO, block);
}
__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c)
{
return ASDisplayNodeFindFirstSubnode(start, ^(ASDisplayNode *n) {
return [n isKindOfClass:c];
});
}
static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendant)
{
ASDisplayNode *supernode = possibleDescendant;
while (supernode) {
if (supernode == possibleAncestor) {
return YES;
}
supernode = supernode.supernode;
}
return NO;
}
UIWindow * _Nullable ASFindWindowOfLayer(CALayer *layer)
{
UIView *view = ASFindClosestViewOfLayer(layer);
if (UIWindow *window = ASDynamicCast(view, UIWindow)) {
return window;
} else {
return view.window;
}
}
UIView * _Nullable ASFindClosestViewOfLayer(CALayer *layer)
{
while (layer != nil) {
if (UIView *view = ASDynamicCast(layer.delegate, UIView)) {
return view;
}
layer = layer.superlayer;
}
return nil;
}
ASDisplayNode *ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2)
{
ASDisplayNode *possibleAncestor = node1;
while (possibleAncestor) {
if (_ASDisplayNodeIsAncestorOfDisplayNode(possibleAncestor, node2)) {
break;
}
possibleAncestor = possibleAncestor.supernode;
}
ASDisplayNodeCAssertNotNil(possibleAncestor, @"Could not find a common ancestor between node1: %@ and node2: %@", node1, node2);
return possibleAncestor;
}
ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node)
{
// node <- supernode on each loop
// previous <- node on each loop where node is not nil
// previous is the final non-nil value of supernode, i.e. the root node
ASDisplayNode *previousNode = node;
while ((node = [node supernode])) {
previousNode = node;
}
return previousNode;
}
#pragma mark - Placeholders
UIColor *ASDisplayNodeDefaultPlaceholderColor()
{
static UIColor *defaultPlaceholderColor;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultPlaceholderColor = [UIColor colorWithWhite:0.95 alpha:1.0];
});
return defaultPlaceholderColor;
}
UIColor *ASDisplayNodeDefaultTintColor()
{
static UIColor *defaultTintColor;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0];
});
return defaultTintColor;
}
#pragma mark - Hierarchy Notifications
void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node)
{
[node __incrementVisibilityNotificationsDisabled];
}
void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node)
{
[node __decrementVisibilityNotificationsDisabled];
}

View File

@ -0,0 +1,407 @@
//
// ASDisplayNodeInternal.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
//
// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode.
// These methods must never be called or overridden by other classes.
//
#import <atomic>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import "ASLayoutTransition.h"
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/_ASTransitionContext.h>
#import <AsyncDisplayKit/ASWeakSet.h>
NS_ASSUME_NONNULL_BEGIN
@protocol _ASDisplayLayerDelegate;
@class _ASDisplayLayer;
@class _ASPendingState;
@class ASNodeController;
struct ASDisplayNodeFlags;
BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector);
BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked);
/// Get the pending view state for the node, creating one if needed.
_ASPendingState * ASDisplayNodeGetPendingState(ASDisplayNode * node);
typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
{
ASDisplayNodeMethodOverrideNone = 0,
ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0,
ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1,
ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2,
ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3,
ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4,
ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5,
ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6,
ASDisplayNodeMethodOverrideCanBecomeFirstResponder= 1 << 7,
ASDisplayNodeMethodOverrideBecomeFirstResponder = 1 << 8,
ASDisplayNodeMethodOverrideCanResignFirstResponder= 1 << 9,
ASDisplayNodeMethodOverrideResignFirstResponder = 1 << 10,
ASDisplayNodeMethodOverrideIsFirstResponder = 1 << 11,
};
typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags)
{
Synchronous = 1 << 0,
YogaLayoutInProgress = 1 << 1,
};
// Can be called without the node's lock. Client is responsible for thread safety.
#define _loaded(node) (node->_layer != nil)
#define checkFlag(flag) ((_atomicFlags.load() & flag) != 0)
// Returns the old value of the flag as a BOOL.
#define setFlag(flag, x) (((x ? _atomicFlags.fetch_or(flag) \
: _atomicFlags.fetch_and(~flag)) & flag) != 0)
AS_EXTERN NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification;
AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp;
// Allow 2^n increments of begin disabling hierarchy notifications
#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4
#define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE)
#define NUM_CLIP_CORNER_LAYERS 4
@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate>
{
@package
AS::RecursiveMutex __instanceLock__;
_ASPendingState *_pendingViewState;
ASInterfaceState _pendingInterfaceState;
ASInterfaceState _preExitingInterfaceState;
UIView *_view;
CALayer *_layer;
std::atomic<ASDisplayNodeAtomicFlags> _atomicFlags;
struct ASDisplayNodeFlags {
// public properties
unsigned viewEverHadAGestureRecognizerAttached:1;
unsigned layerBacked:1;
unsigned displaysAsynchronously:1;
unsigned rasterizesSubtree:1;
unsigned shouldBypassEnsureDisplay:1;
unsigned displaySuspended:1;
unsigned shouldAnimateSizeChanges:1;
// Wrapped view handling
// The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically
// optimized for performance and does not use the usual way to provide the contents of the CALayer via the
// CALayerDelegate method that backs the UIImageView.
unsigned canClearContentsOfLayer:1;
// Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer
// triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView
// it goes through the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately
// UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the
// methods at all instead it throws away the contents of the layer and nothing will show up.
unsigned canCallSetNeedsDisplayOfLayer:1;
unsigned implementsDrawRect:1;
unsigned implementsImageDisplay:1;
unsigned implementsDrawParameters:1;
// internal state
unsigned isEnteringHierarchy:1;
unsigned isExitingHierarchy:1;
unsigned isInHierarchy:1;
unsigned visibilityNotificationsDisabled:VISIBILITY_NOTIFICATIONS_DISABLED_BITS;
unsigned isDeallocating:1;
} _flags;
@protected
ASDisplayNode * __weak _supernode;
NSMutableArray<ASDisplayNode *> *_subnodes;
ASNodeController *_strongNodeController;
__weak ASNodeController *_weakNodeController;
// Set this to nil whenever you modify _subnodes
NSArray<ASDisplayNode *> *_cachedSubnodes;
std::atomic_uint _displaySentinel;
// This is the desired contentsScale, not the scale at which the layer's contents should be displayed
CGFloat _contentsScaleForDisplay;
ASDisplayNodeMethodOverrides _methodOverrides;
UIEdgeInsets _hitTestSlop;
#if ASEVENTLOG_ENABLE
ASEventLog *_eventLog;
#endif
// Layout support
ASLayoutElementStyle *_style;
std::atomic<ASPrimitiveTraitCollection> _primitiveTraitCollection;
// Layout Spec
ASLayoutSpecBlock _layoutSpecBlock;
NSString *_debugName;
#if YOGA
// Only ASDisplayNodes are supported in _yogaChildren currently. This means that it is necessary to
// create ASDisplayNodes to make a stack layout when using Yoga.
// However, the implementation is mostly ready for id <ASLayoutElement>, with a few areas requiring updates.
NSMutableArray<ASDisplayNode *> *_yogaChildren;
__weak ASDisplayNode *_yogaParent;
ASLayout *_yogaCalculatedLayout;
#endif
// Automatically manages subnodes
BOOL _automaticallyManagesSubnodes; // Main thread only
// Layout Transition
_ASTransitionContext *_pendingLayoutTransitionContext;
NSTimeInterval _defaultLayoutTransitionDuration;
NSTimeInterval _defaultLayoutTransitionDelay;
UIViewAnimationOptions _defaultLayoutTransitionOptions;
std::atomic<int32_t> _transitionID;
std::atomic<int32_t> _pendingTransitionID;
ASLayoutTransition *_pendingLayoutTransition;
ASDisplayNodeLayout _calculatedDisplayNodeLayout;
ASDisplayNodeLayout _pendingDisplayNodeLayout;
/// Sentinel for layout data. Incremented when we get -setNeedsLayout / -invalidateCalculatedLayout.
/// Starts at 1.
std::atomic<NSUInteger> _layoutVersion;
// Layout Spec performance measurement
ASDisplayNodePerformanceMeasurementOptions _measurementOptions;
NSTimeInterval _layoutSpecTotalTime;
NSInteger _layoutSpecNumberOfPasses;
NSTimeInterval _layoutComputationTotalTime;
NSInteger _layoutComputationNumberOfPasses;
// View Loading
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
NSMutableArray<ASDisplayNodeDidLoadBlock> *_onDidLoadBlocks;
Class _viewClass; // nil -> _ASDisplayView
Class _layerClass; // nil -> _ASDisplayLayer
// Placeholder support
UIImage *_placeholderImage;
BOOL _placeholderEnabled;
CALayer *_placeholderLayer;
// keeps track of nodes/subnodes that have not finished display, used with placeholders
ASWeakSet *_pendingDisplayNodes;
// Corner Radius support
CGFloat _cornerRadius;
ASCornerRoundingType _cornerRoundingType;
CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS];
ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext;
ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext;
// Accessibility support
BOOL _isAccessibilityElement;
NSString *_accessibilityLabel;
NSAttributedString *_accessibilityAttributedLabel;
NSString *_accessibilityHint;
NSAttributedString *_accessibilityAttributedHint;
NSString *_accessibilityValue;
NSAttributedString *_accessibilityAttributedValue;
UIAccessibilityTraits _accessibilityTraits;
CGRect _accessibilityFrame;
NSString *_accessibilityLanguage;
BOOL _accessibilityElementsHidden;
BOOL _accessibilityViewIsModal;
BOOL _shouldGroupAccessibilityChildren;
NSString *_accessibilityIdentifier;
UIAccessibilityNavigationStyle _accessibilityNavigationStyle;
NSArray *_accessibilityHeaderElements;
CGPoint _accessibilityActivationPoint;
UIBezierPath *_accessibilityPath;
BOOL _isAccessibilityContainer;
// Safe Area support
// These properties are used on iOS 10 and lower, where safe area is not supported by UIKit.
UIEdgeInsets _fallbackSafeAreaInsets;
BOOL _fallbackInsetsLayoutMarginsFromSafeArea;
BOOL _automaticallyRelayoutOnSafeAreaChanges;
BOOL _automaticallyRelayoutOnLayoutMarginsChanges;
BOOL _isViewControllerRoot;
#pragma mark - ASDisplayNode (Debugging)
ASLayout *_unflattenedLayout;
#if TIME_DISPLAYNODE_OPS
@public
NSTimeInterval _debugTimeToCreateView;
NSTimeInterval _debugTimeToApplyPendingState;
NSTimeInterval _debugTimeToAddSubnodeViews;
NSTimeInterval _debugTimeForDidLoad;
#endif
/// Fast path: tells whether we've ever had an interface state delegate before.
BOOL _hasHadInterfaceStateDelegates;
__weak id<ASInterfaceStateDelegate> _interfaceStateDelegates[AS_MAX_INTERFACE_STATE_DELEGATES];
}
+ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node;
/// The _ASDisplayLayer backing the node, if any.
@property (nullable, nonatomic, readonly) _ASDisplayLayer *asyncLayer;
/// Bitmask to check which methods an object overrides.
- (ASDisplayNodeMethodOverrides)methodOverrides;
/**
* Invoked before a call to setNeedsLayout to the underlying view
*/
- (void)__setNeedsLayout;
/**
* Invoked after a call to setNeedsDisplay to the underlying view
*/
- (void)__setNeedsDisplay;
/**
* Called whenever the node needs to layout its subnodes and, if it's already loaded, its subviews. Executes the layout pass for the node
*
* This method is thread-safe but asserts thread affinity.
*/
- (void)__layout;
/**
* Internal method to add / replace / insert subnode and remove from supernode without checking if
* node has automaticallyManagesSubnodes set to YES.
*/
- (void)_addSubnode:(ASDisplayNode *)subnode;
- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode;
- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below;
- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above;
- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx;
- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode;
- (void)_removeFromSupernode;
// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this.
- (BOOL)__visibilityNotificationsDisabled;
- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled;
- (void)__incrementVisibilityNotificationsDisabled;
- (void)__decrementVisibilityNotificationsDisabled;
// Helper methods for UIResponder forwarding
- (BOOL)__canBecomeFirstResponder;
- (BOOL)__becomeFirstResponder;
- (BOOL)__canResignFirstResponder;
- (BOOL)__resignFirstResponder;
- (BOOL)__isFirstResponder;
/// Helper method to summarize whether or not the node run through the display process
- (BOOL)_implementsDisplay;
/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated.
- (void)displayImmediately;
/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type.
- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius;
/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses.
- (instancetype)initWithViewClass:(Class)viewClass;
/// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses.
- (instancetype)initWithLayerClass:(Class)layerClass;
@property (nonatomic) CGFloat contentsScaleForDisplay;
- (void)applyPendingViewState;
/**
* Makes a local copy of the interface state delegates then calls the block on each.
*
* Lock is not held during block invocation. Method must not be called with the lock held.
*/
- (void)enumerateInterfaceStateDelegates:(void(NS_NOESCAPE ^)(id<ASInterfaceStateDelegate> delegate))block;
/**
* // TODO: NOT YET IMPLEMENTED
*
* @abstract Prevents interface state changes from affecting the node, until disabled.
*
* @discussion Useful to avoid flashing after removing a node from the hierarchy and re-adding it.
* Removing a node from the hierarchy will cause it to exit the Display state, clearing its contents.
* For some animations, it's desirable to be able to remove a node without causing it to re-display.
* Once re-enabled, the interface state will be updated to the same value it would have been.
*
* @see ASInterfaceState
*/
@property (nonatomic) BOOL interfaceStateSuspended;
/**
* This method has proven helpful in a few rare scenarios, similar to a category extension on UIView,
* but it's considered private API for now and its use should not be encouraged.
* @param checkViewHierarchy If YES, and no supernode can be found, method will walk up from `self.view` to find a supernode.
* If YES, this method must be called on the main thread and the node must not be layer-backed.
*/
- (nullable ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy;
/**
* Whether this node rasterizes its descendants. See -enableSubtreeRasterization.
*/
@property (readonly) BOOL rasterizesSubtree;
/**
* Called if a gesture recognizer was attached to an _ASDisplayView
*/
- (void)nodeViewDidAddGestureRecognizer;
// Recalculates fallbackSafeAreaInsets for the subnodes
- (void)_fallbackUpdateSafeAreaOnChildren;
@end
@interface ASDisplayNode (InternalPropertyBridge)
@property (nonatomic) CGFloat layerCornerRadius;
- (BOOL)_locked_insetsLayoutMarginsFromSafeArea;
@end
@interface ASDisplayNode (ASLayoutElementPrivate)
/**
* Returns the internal style object or creates a new if no exists. Need to be called with lock held.
*/
- (ASLayoutElementStyle *)_locked_style;
/**
* Returns the current layout element. Need to be called with lock held.
*/
- (id<ASLayoutElement>)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,59 @@
//
// ASDisplayNodeLayout.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#pragma once
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDimension.h>
@class ASLayout;
/*
* Represents a connection between an ASLayout and a ASDisplayNode
* ASDisplayNode uses this to store additional information that are necessary besides the layout
*/
struct ASDisplayNodeLayout {
ASLayout *layout;
ASSizeRange constrainedSize;
CGSize parentSize;
BOOL requestedLayoutFromAbove;
NSUInteger version;
/*
* Create a new display node layout with
* @param layout The layout to associate, usually returned from a call to -layoutThatFits:parentSize:
* @param constrainedSize Constrained size used to create the layout
* @param parentSize Parent size used to create the layout
* @param version The version of the source layout data see ASDisplayNode's _layoutVersion.
*/
ASDisplayNodeLayout(ASLayout *layout, ASSizeRange constrainedSize, CGSize parentSize, NSUInteger version)
: layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), requestedLayoutFromAbove(NO), version(version) {};
/*
* Creates a layout without any layout associated. By default this display node layout is dirty.
*/
ASDisplayNodeLayout()
: layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), version(0) {};
/**
* Returns whether this is valid for a given version
*/
BOOL isValid(NSUInteger versionArg) {
return layout != nil && version >= versionArg;
}
/**
* Returns whether this is valid for a given constrained size, parent size, and version
*/
BOOL isValid(ASSizeRange theConstrainedSize, CGSize theParentSize, NSUInteger versionArg) {
return isValid(versionArg)
&& CGSizeEqualToSize(parentSize, theParentSize)
&& ASSizeRangeEqualToSizeRange(constrainedSize, theConstrainedSize);
}
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
//
// ASExperimentalFeatures.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASExperimentalFeatures.h>
#import <AsyncDisplayKit/ASCollections.h>
NSArray<NSString *> *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags)
{
NSArray *allNames = ASCreateOnce((@[@"exp_graphics_contexts",
@"exp_text_node",
@"exp_interface_state_coalesce",
@"exp_unfair_lock",
@"exp_infer_layer_defaults",
@"exp_collection_teardown",
@"exp_framesetter_cache",
@"exp_skip_clear_data",
@"exp_did_enter_preload_skip_asm_layout",
@"exp_disable_a11y_cache",
@"exp_dispatch_apply",
@"exp_image_downloader_priority",
@"exp_text_drawing"]));
if (flags == ASExperimentalFeatureAll) {
return allNames;
}
// Go through all names, testing each bit.
NSUInteger i = 0;
return ASArrayByFlatMapping(allNames, NSString *name, ({
(flags & (1 << i++)) ? name : nil;
}));
}
// O(N^2) but with counts this small, it's probably faster
// than hashing the strings.
ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray<NSString *> *array)
{
NSArray *allNames = ASExperimentalFeaturesGetNames(ASExperimentalFeatureAll);
ASExperimentalFeatures result = 0;
for (NSString *str in array) {
NSUInteger i = [allNames indexOfObject:str];
if (i != NSNotFound) {
result |= (1 << i);
}
}
return result;
}

View File

@ -0,0 +1,167 @@
//
// ASGraphicsContext.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import "ASCGImageBuffer.h"
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <UIKit/UIGraphics.h>
#import <UIKit/UIImage.h>
#import <objc/runtime.h>
/**
* Our version of the private CGBitmapGetAlignedBytesPerRow function.
*
* In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32
* in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that
* the bytes-per-row for a 1x1 context from the system is 32.
*/
static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) {
// Add 31 then zero out low 5 bits.
return (baseValue + 31) & ~0x1F;
}
/**
* A key used to associate CGContextRef -> NSMutableData, nonatomic retain.
*
* That way the data will be released when the context dies. If they pull an image,
* we will retain the data object (in a CGDataProvider) before releasing the context.
*/
static UInt8 __contextDataAssociationKey;
#pragma mark - Graphics Contexts
void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
return;
}
// We use "reference contexts" to get device-specific options that UIKit
// uses.
static dispatch_once_t onceToken;
static CGContextRef refCtxOpaque;
static CGContextRef refCtxTransparent;
dispatch_once(&onceToken, ^{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1);
refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext());
ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?");
UIGraphicsEndImageContext();
// Make transparent ref context.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1);
refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext());
UIGraphicsEndImageContext();
});
// These options are taken from UIGraphicsBeginImageContext.
CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent;
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx);
if (scale == 0) {
scale = ASScreenScale();
}
size_t intWidth = (size_t)ceil(size.width * scale);
size_t intHeight = (size_t)ceil(size.height * scale);
size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx);
size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8;
bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow);
size_t bufferSize = bytesPerRow * intHeight;
CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx);
// We create our own buffer, and wrap the context around that. This way we can prevent
// the copy that usually gets made when you form a CGImage from the context.
ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize];
CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
// Transfer ownership of the data to the context. So that if the context
// is destroyed before we create an image from it, the data will be released.
objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Set the CTM to account for iOS orientation & specified scale.
// If only we could use CGContextSetBaseCTM. It doesn't
// seem like there are any consequences for our use case
// but we'll be on the look out. The internet hinted that it
// affects shadowing but I tested and shadowing works.
CGContextTranslateCTM(context, 0, intHeight);
CGContextScaleCTM(context, scale, -scale);
// Save the state so we can restore it and recover our scale in GetImageAndEnd
CGContextSaveGState(context);
// Transfer context ownership to the UIKit stack.
UIGraphicsPushContext(context);
CGContextRelease(context);
}
UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
// Pop the context and make sure we have one.
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL) {
ASDisplayNodeCFailAssert(@"Can't end image context without having begun one.");
return nil;
}
// Read the device-specific ICC-based color space to use for the image.
// For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage
// generates an image in a device-specific color space (for wide color support).
// We replicate that behavior, even though at this time CA does not
// require the image to be in this space. Plain DeviceRGB images seem
// to be treated exactly the same, but better safe than sorry.
static CGColorSpaceRef imageColorSpace;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext();
imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage));
ASDisplayNodeCAssertNotNil(imageColorSpace, nil);
UIGraphicsEndImageContext();
});
// Retrieve our buffer and create a CGDataProvider from it.
ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey);
ASDisplayNodeCAssertNotNil(buffer, nil);
CGDataProviderRef provider = [buffer createDataProviderAndInvalidate];
// Create the CGImage. Options taken from CGBitmapContextCreateImage.
CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(provider);
// We saved our GState right after setting the CTM so that we could restore it
// here and get the original scale back.
CGContextRestoreGState(context);
CGFloat scale = CGContextGetCTM(context).a;
// Note: popping from the UIKit stack will probably destroy the context.
context = NULL;
UIGraphicsPopContext();
UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp];
CGImageRelease(cgImg);
return result;
}
void ASGraphicsEndImageContext()
{
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
UIGraphicsEndImageContext();
return;
}
UIGraphicsPopContext();
}

View File

@ -0,0 +1,38 @@
//
// ASHashing.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASHashing.h>
#define ELF_STEP(B) T1 = (H << 4) + B; T2 = T1 & 0xF0000000; if (T2) T1 ^= (T2 >> 24); T1 &= (~T2); H = T1;
/**
* The hashing algorithm copied from CoreFoundation CFHashBytes function.
* https://opensource.apple.com/source/CF/CF-1153.18/CFUtilities.c.auto.html
*/
NSUInteger ASHashBytes(void *bytesarg, size_t length) {
/* The ELF hash algorithm, used in the ELF object file format */
uint8_t *bytes = (uint8_t *)bytesarg;
UInt32 H = 0, T1, T2;
SInt32 rem = (SInt32)length;
while (3 < rem) {
ELF_STEP(bytes[length - rem]);
ELF_STEP(bytes[length - rem + 1]);
ELF_STEP(bytes[length - rem + 2]);
ELF_STEP(bytes[length - rem + 3]);
rem -= 4;
}
switch (rem) {
case 3: ELF_STEP(bytes[length - 3]);
case 2: ELF_STEP(bytes[length - 2]);
case 1: ELF_STEP(bytes[length - 1]);
case 0: ;
}
return H;
}
#undef ELF_STEP

View File

@ -0,0 +1,233 @@
//
// ASInternalHelpers.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#import <cmath>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASThread.h>
static NSNumber *allowsGroupOpacityFromUIKitOrNil;
static NSNumber *allowsEdgeAntialiasingFromUIKitOrNil;
BOOL ASDefaultAllowsGroupOpacity()
{
static BOOL groupOpacity;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSNumber *groupOpacityObj = allowsGroupOpacityFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewGroupOpacity"];
groupOpacity = groupOpacityObj ? groupOpacityObj.boolValue : YES;
});
return groupOpacity;
}
BOOL ASDefaultAllowsEdgeAntialiasing()
{
static BOOL edgeAntialiasing;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSNumber *antialiasingObj = allowsEdgeAntialiasingFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewEdgeAntialiasing"];
edgeAntialiasing = antialiasingObj ? antialiasingObj.boolValue : NO;
});
return edgeAntialiasing;
}
void ASInitializeFrameworkMainThread(void)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ASDisplayNodeCAssertMainThread();
// Ensure these values are cached on the main thread before needed in the background.
if (ASActivateExperimentalFeature(ASExperimentalLayerDefaults)) {
// Nop. We will gather default values on-demand in ASDefaultAllowsGroupOpacity and ASDefaultAllowsEdgeAntialiasing
} else {
CALayer *layer = [[[UIView alloc] init] layer];
allowsGroupOpacityFromUIKitOrNil = @(layer.allowsGroupOpacity);
allowsEdgeAntialiasingFromUIKitOrNil = @(layer.allowsEdgeAntialiasing);
}
ASNotifyInitialized();
});
}
BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector)
{
if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself.
Method superclassMethod = class_getInstanceMethod(superclass, selector);
Method subclassMethod = class_getInstanceMethod(subclass, selector);
return (superclassMethod != subclassMethod);
}
BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector)
{
if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself.
Method superclassMethod = class_getClassMethod(superclass, selector);
Method subclassMethod = class_getClassMethod(subclass, selector);
return (superclassMethod != subclassMethod);
}
IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block)
{
NSCParameterAssert(block);
// Get original method
Method origMethod = class_getInstanceMethod(c, origSEL);
NSCParameterAssert(origMethod);
// Convert block to IMP trampoline and replace method implementation
IMP newIMP = imp_implementationWithBlock(block);
// Try adding the method if not yet in the current class
if (!class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) {
return method_setImplementation(origMethod, newIMP);
} else {
return method_getImplementation(origMethod);
}
}
void ASPerformBlockOnMainThread(void (^block)(void))
{
if (block == nil){
return;
}
if (ASDisplayNodeThreadIsMain()) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
}
void ASPerformBlockOnBackgroundThread(void (^block)(void))
{
if (block == nil){
return;
}
if (ASDisplayNodeThreadIsMain()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
} else {
block();
}
}
void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object)
{
[[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object];
}
Class _Nullable ASGetClassFromType(const char * _Nullable type)
{
// Class types all start with @"
if (type == NULL || strncmp(type, "@\"", 2) != 0) {
return Nil;
}
// Ensure length >= 3
size_t typeLength = strlen(type);
if (typeLength < 3) {
ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type);
return Nil;
}
// Copy type[2..(end-1)]. So @"UIImage" -> UIImage
size_t resultLength = typeLength - 3;
char className[resultLength + 1];
strncpy(className, type + 2, resultLength);
className[resultLength] = '\0';
return objc_getClass(className);
}
CGFloat ASScreenScale()
{
static CGFloat __scale = 0.0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
__scale = CGContextGetCTM(UIGraphicsGetCurrentContext()).a;
UIGraphicsEndImageContext();
});
return __scale;
}
CGSize ASFloorSizeValues(CGSize s)
{
return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height));
}
// See ASCeilPixelValue for a more thoroguh explanation of (f + FLT_EPSILON),
// but here is some quick math:
//
// Imagine a layout that comes back with a height of 100.66666666663
// for a 3x deice:
// 100.66666666663 * 3 = 301.99999999988995
// floor(301.99999999988995) = 301
// 301 / 3 = 100.333333333
//
// If we add FLT_EPSILON to normalize the garbage at the end we get:
// po (100.66666666663 + FLT_EPSILON) * 3 = 302.00000035751782
// floor(302.00000035751782) = 302
// 302/3 = 100.66666666
CGFloat ASFloorPixelValue(CGFloat f)
{
CGFloat scale = ASScreenScale();
return floor((f + FLT_EPSILON) * scale) / scale;
}
CGPoint ASCeilPointValues(CGPoint p)
{
return CGPointMake(ASCeilPixelValue(p.x), ASCeilPixelValue(p.y));
}
CGSize ASCeilSizeValues(CGSize s)
{
return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height));
}
// With 3x devices layouts will often to compute to pixel bounds but
// include garbage values beyond the precision of a float/double.
// This garbage can result in a pixel value being rounded up when it isn't
// necessary.
//
// For example, imagine a layout that comes back with a height of 100.666666666669
// for a 3x device:
// 100.666666666669 * 3 = 302.00000000000699
// ceil(302.00000000000699) = 303
// 303/3 = 101
//
// If we use FLT_EPSILON to get rid of the garbage at the end of the value,
// things work as expected:
// (100.666666666669 - FLT_EPSILON) * 3 = 301.99999964237912
// ceil(301.99999964237912) = 302
// 302/3 = 100.666666666
//
// For even more conversation around this, see:
// https://github.com/TextureGroup/Texture/issues/838
CGFloat ASCeilPixelValue(CGFloat f)
{
CGFloat scale = ASScreenScale();
return ceil((f - FLT_EPSILON) * scale) / scale;
}
CGFloat ASRoundPixelValue(CGFloat f)
{
CGFloat scale = ASScreenScale();
return round(f * scale) / scale;
}
@implementation NSIndexPath (ASInverseComparison)
- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath
{
return [otherIndexPath compare:self];
}
@end

View File

@ -0,0 +1,378 @@
//
// ASLayout.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASLayout.h>
#import <atomic>
#import <queue>
#import <AsyncDisplayKit/ASCollections.h>
#import <AsyncDisplayKit/ASDimension.h>
#import "ASLayoutSpecUtilities.h"
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey";
CGPoint const ASPointNull = {NAN, NAN};
BOOL ASPointIsNull(CGPoint point)
{
return isnan(point.x) && isnan(point.y);
}
/**
* Creates an defined number of " |" indent blocks for the recursive description.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents)
{
NSMutableString *description = [NSMutableString string];
for (NSUInteger i = 0; i < indents; i++) {
[description appendString:@" |"];
}
if (indents > 0) {
[description appendString:@" "];
}
return description;
}
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout)
{
return layout.type == ASLayoutElementTypeDisplayNode;
}
@interface ASLayout () <ASDescriptionProvider>
{
ASLayoutElementType _layoutElementType;
std::atomic_bool _retainSublayoutElements;
}
@end
@implementation ASLayout
@dynamic frame, type;
static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(NO);
+ (void)setShouldRetainSublayoutLayoutElements:(BOOL)shouldRetain
{
static_retainsSublayoutLayoutElements.store(shouldRetain);
}
+ (BOOL)shouldRetainSublayoutLayoutElements
{
return static_retainsSublayoutLayoutElements.load();
}
- (instancetype)initWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts
{
NSParameterAssert(layoutElement);
self = [super init];
if (self) {
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
for (ASLayout *sublayout in sublayouts) {
ASDisplayNodeAssert(ASPointIsNull(sublayout.position) == NO, @"Invalid position is not allowed in sublayout.");
}
#endif
_layoutElement = layoutElement;
// Read this now to avoid @c weak overhead later.
_layoutElementType = layoutElement.layoutElementType;
if (!ASIsCGSizeValidForSize(size)) {
//ASDisplayNodeFailAssert(@"layoutSize is invalid and unsafe to provide to Core Animation! Release configurations will force to 0, 0. Size = %@, node = %@", NSStringFromCGSize(size), layoutElement);
size = CGSizeZero;
} else {
size = CGSizeMake(ASCeilPixelValue(size.width), ASCeilPixelValue(size.height));
}
_size = size;
if (ASPointIsNull(position) == NO) {
_position = ASCeilPointValues(position);
} else {
_position = position;
}
_sublayouts = [sublayouts copy] ?: @[];
if ([ASLayout shouldRetainSublayoutLayoutElements]) {
[self retainSublayoutElements];
}
}
return self;
}
#pragma mark - Class Constructors
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElement:layoutElement
size:size
position:position
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [self layoutWithLayoutElement:layoutElement
size:size
position:ASPointNull
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size NS_RETURNS_RETAINED
{
return [self layoutWithLayoutElement:layoutElement
size:size
position:ASPointNull
sublayouts:nil];
}
- (void)dealloc
{
if (_retainSublayoutElements.load()) {
for (ASLayout *sublayout in _sublayouts) {
// We retained this, so there's no risk of it deallocating on us.
if (CFTypeRef cfElement = (__bridge CFTypeRef)sublayout->_layoutElement) {
CFRelease(cfElement);
}
}
}
}
#pragma mark - Sublayout Elements Caching
- (void)retainSublayoutElements
{
if (_retainSublayoutElements.exchange(true)) {
return;
}
for (ASLayout *sublayout in _sublayouts) {
// CFBridgingRetain atomically casts and retains. We need the atomicity.
CFBridgingRetain(sublayout->_layoutElement);
}
}
#pragma mark - Layout Flattening
- (BOOL)isFlattened
{
// A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts.
if (!ASPointIsNull(_position)) {
return NO;
}
for (ASLayout *sublayout in _sublayouts) {
if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout->_sublayouts.count > 0) {
return NO;
}
}
return YES;
}
- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED
{
if ([self isFlattened]) {
// All flattened layouts must retain sublayout elements until they are applied.
[self retainSublayoutElements];
return self;
}
struct Context {
unowned ASLayout *layout;
CGPoint absolutePosition;
};
// Queue used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::deque<Context> queue;
for (ASLayout *sublayout in _sublayouts) {
queue.push_back({sublayout, sublayout.position});
}
std::vector<ASLayout *> flattenedSublayouts;
while (!queue.empty()) {
const Context context = std::move(queue.front());
queue.pop_front();
unowned ASLayout *layout = context.layout;
// Direct ivar access to avoid retain/release, use existing +1.
const NSUInteger sublayoutsCount = layout->_sublayouts.count;
const CGPoint absolutePosition = context.absolutePosition;
if (ASLayoutIsDisplayNodeType(layout)) {
if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) {
// Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position.
const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement
size:layout.size
position:absolutePosition
sublayouts:@[]];
flattenedSublayouts.push_back(newLayout);
} else {
flattenedSublayouts.push_back(layout);
}
} else if (sublayoutsCount > 0) {
// Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue.
unowned ASLayout *rawSublayouts[sublayoutsCount];
[layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)];
for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) {
queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position});
}
}
}
NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()];
// flattenedSublayouts is now all nils.
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array];
// All flattened layouts must retain sublayout elements until they are applied.
[layout retainSublayoutElements];
return layout;
}
#pragma mark - Equality Checking
- (BOOL)isEqual:(id)object
{
if (self == object) return YES;
ASLayout *layout = ASDynamicCast(object, ASLayout);
if (layout == nil) {
return NO;
}
if (!CGSizeEqualToSize(_size, layout.size)) return NO;
if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position))
|| CGPointEqualToPoint(self.position, layout.position))) return NO;
if (_layoutElement != layout.layoutElement) return NO;
if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) {
return NO;
}
return YES;
}
#pragma mark - Accessors
- (ASLayoutElementType)type
{
return _layoutElementType;
}
- (CGRect)frameForElement:(id<ASLayoutElement>)layoutElement
{
for (ASLayout *l in _sublayouts) {
if (l->_layoutElement == layoutElement) {
return l.frame;
}
}
return CGRectNull;
}
- (CGRect)frame
{
CGRect subnodeFrame = CGRectZero;
CGPoint adjustedOrigin = _position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedOrigin.x = 0;
}
if (isfinite(adjustedOrigin.y) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedOrigin.y = 0;
}
subnodeFrame.origin = adjustedOrigin;
CGSize adjustedSize = _size;
if (isfinite(adjustedSize.width) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid size");
adjustedSize.width = 0;
}
if (isfinite(adjustedSize.height) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedSize.height = 0;
}
subnodeFrame.size = adjustedSize;
return subnodeFrame;
}
#pragma mark - Description
- (NSMutableArray <NSDictionary *> *)propertiesForDescription
{
NSMutableArray *result = [NSMutableArray array];
[result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }];
if (id<ASLayoutElement> layoutElement = self.layoutElement) {
[result addObject:@{ @"layoutElement" : layoutElement }];
}
const auto pos = self.position;
if (!ASPointIsNull(pos)) {
[result addObject:@{ @"position" : [NSValue valueWithCGPoint:pos] }];
}
return result;
}
- (NSString *)description
{
return ASObjectDescriptionMake(self, [self propertiesForDescription]);
}
- (NSString *)recursiveDescription
{
return [self _recursiveDescriptionForLayout:self level:0];
}
- (NSString *)_recursiveDescriptionForLayout:(ASLayout *)layout level:(NSUInteger)level
{
NSMutableString *description = [NSMutableString string];
[description appendString:descriptionIndents(level)];
[description appendString:[layout description]];
for (ASLayout *sublayout in layout.sublayouts) {
[description appendString:@"\n"];
[description appendString:[self _recursiveDescriptionForLayout:sublayout level:level + 1]];
}
return description;
}
@end
ASLayout *ASCalculateLayout(id<ASLayoutElement> layoutElement, const ASSizeRange sizeRange, const CGSize parentSize)
{
NSCParameterAssert(layoutElement != nil);
return [layoutElement layoutThatFits:sizeRange parentSize:parentSize];
}
ASLayout *ASCalculateRootLayout(id<ASLayoutElement> rootLayoutElement, const ASSizeRange sizeRange)
{
ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max);
// Here could specific verfication happen
return layout;
}

View File

@ -0,0 +1,843 @@
//
// ASLayoutElement.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <atomic>
#include <pthread.h>
using AS::MutexLocker;
#if YOGA
#import YOGA_HEADER_PATH
#import <AsyncDisplayKit/ASYogaUtilities.h>
#endif
#pragma mark - ASLayoutElementContext
@implementation ASLayoutElementContext
- (instancetype)init
{
if (self = [super init]) {
_transitionID = ASLayoutElementContextDefaultTransitionID;
}
return self;
}
@end
CGFloat const ASLayoutElementParentDimensionUndefined = NAN;
CGSize const ASLayoutElementParentSizeUndefined = {ASLayoutElementParentDimensionUndefined, ASLayoutElementParentDimensionUndefined};
int32_t const ASLayoutElementContextInvalidTransitionID = 0;
int32_t const ASLayoutElementContextDefaultTransitionID = ASLayoutElementContextInvalidTransitionID + 1;
#if AS_TLS_AVAILABLE
static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context;
void ASLayoutElementPushContext(ASLayoutElementContext *context)
{
// NOTE: It would be easy to support nested contexts just use an NSMutableArray here.
ASDisplayNodeCAssertNil(tls_context, @"Nested ASLayoutElementContexts aren't supported.");
tls_context = (__bridge ASLayoutElementContext *)(__bridge_retained CFTypeRef)context;
}
ASLayoutElementContext *ASLayoutElementGetCurrentContext()
{
// Don't retain here. Caller will retain if it wants to!
return tls_context;
}
void ASLayoutElementPopContext()
{
ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!");
CFRelease((__bridge CFTypeRef)tls_context);
tls_context = nil;
}
#else
static pthread_key_t ASLayoutElementContextKey() {
static pthread_key_t k;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_key_create(&k, NULL);
});
return k;
}
void ASLayoutElementPushContext(ASLayoutElementContext *context)
{
// NOTE: It would be easy to support nested contexts just use an NSMutableArray here.
ASDisplayNodeCAssertNil(pthread_getspecific(ASLayoutElementContextKey()), @"Nested ASLayoutElementContexts aren't supported.");
const auto cfCtx = (__bridge_retained CFTypeRef)context;
pthread_setspecific(ASLayoutElementContextKey(), cfCtx);
}
ASLayoutElementContext *ASLayoutElementGetCurrentContext()
{
// Don't retain here. Caller will retain if it wants to!
const auto ctxPtr = pthread_getspecific(ASLayoutElementContextKey());
return (__bridge ASLayoutElementContext *)ctxPtr;
}
void ASLayoutElementPopContext()
{
const auto ctx = (CFTypeRef)pthread_getspecific(ASLayoutElementContextKey());
ASDisplayNodeCAssertNotNil(ctx, @"Attempt to pop context when there wasn't a context!");
CFRelease(ctx);
pthread_setspecific(ASLayoutElementContextKey(), NULL);
}
#endif // AS_TLS_AVAILABLE
#pragma mark - ASLayoutElementStyle
NSString * const ASLayoutElementStyleWidthProperty = @"ASLayoutElementStyleWidthProperty";
NSString * const ASLayoutElementStyleMinWidthProperty = @"ASLayoutElementStyleMinWidthProperty";
NSString * const ASLayoutElementStyleMaxWidthProperty = @"ASLayoutElementStyleMaxWidthProperty";
NSString * const ASLayoutElementStyleHeightProperty = @"ASLayoutElementStyleHeightProperty";
NSString * const ASLayoutElementStyleMinHeightProperty = @"ASLayoutElementStyleMinHeightProperty";
NSString * const ASLayoutElementStyleMaxHeightProperty = @"ASLayoutElementStyleMaxHeightProperty";
NSString * const ASLayoutElementStyleSpacingBeforeProperty = @"ASLayoutElementStyleSpacingBeforeProperty";
NSString * const ASLayoutElementStyleSpacingAfterProperty = @"ASLayoutElementStyleSpacingAfterProperty";
NSString * const ASLayoutElementStyleFlexGrowProperty = @"ASLayoutElementStyleFlexGrowProperty";
NSString * const ASLayoutElementStyleFlexShrinkProperty = @"ASLayoutElementStyleFlexShrinkProperty";
NSString * const ASLayoutElementStyleFlexBasisProperty = @"ASLayoutElementStyleFlexBasisProperty";
NSString * const ASLayoutElementStyleAlignSelfProperty = @"ASLayoutElementStyleAlignSelfProperty";
NSString * const ASLayoutElementStyleAscenderProperty = @"ASLayoutElementStyleAscenderProperty";
NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleDescenderProperty";
NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementStyleLayoutPositionProperty";
#if YOGA
NSString * const ASYogaFlexWrapProperty = @"ASLayoutElementStyleLayoutFlexWrapProperty";
NSString * const ASYogaFlexDirectionProperty = @"ASYogaFlexDirectionProperty";
NSString * const ASYogaDirectionProperty = @"ASYogaDirectionProperty";
NSString * const ASYogaSpacingProperty = @"ASYogaSpacingProperty";
NSString * const ASYogaJustifyContentProperty = @"ASYogaJustifyContentProperty";
NSString * const ASYogaAlignItemsProperty = @"ASYogaAlignItemsProperty";
NSString * const ASYogaPositionTypeProperty = @"ASYogaPositionTypeProperty";
NSString * const ASYogaPositionProperty = @"ASYogaPositionProperty";
NSString * const ASYogaMarginProperty = @"ASYogaMarginProperty";
NSString * const ASYogaPaddingProperty = @"ASYogaPaddingProperty";
NSString * const ASYogaBorderProperty = @"ASYogaBorderProperty";
NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty";
#endif
#define ASLayoutElementStyleSetSizeWithScope(x) \
__instanceLock__.lock(); \
ASLayoutElementSize newSize = _size.load(); \
{ x } \
_size.store(newSize); \
__instanceLock__.unlock();
#define ASLayoutElementStyleCallDelegate(propertyName)\
do {\
[self propertyDidChange:propertyName];\
[_delegate style:self propertyDidChange:propertyName];\
} while(0)
@implementation ASLayoutElementStyle {
AS::RecursiveMutex __instanceLock__;
ASLayoutElementStyleExtensions _extensions;
std::atomic<ASLayoutElementSize> _size;
std::atomic<CGFloat> _spacingBefore;
std::atomic<CGFloat> _spacingAfter;
std::atomic<CGFloat> _flexGrow;
std::atomic<CGFloat> _flexShrink;
std::atomic<ASDimension> _flexBasis;
std::atomic<ASStackLayoutAlignSelf> _alignSelf;
std::atomic<CGFloat> _ascender;
std::atomic<CGFloat> _descender;
std::atomic<CGPoint> _layoutPosition;
#if YOGA
YGNodeRef _yogaNode;
std::atomic<YGWrap> _flexWrap;
std::atomic<ASStackLayoutDirection> _flexDirection;
std::atomic<YGDirection> _direction;
std::atomic<ASStackLayoutJustifyContent> _justifyContent;
std::atomic<ASStackLayoutAlignItems> _alignItems;
std::atomic<YGPositionType> _positionType;
std::atomic<ASEdgeInsets> _position;
std::atomic<ASEdgeInsets> _margin;
std::atomic<ASEdgeInsets> _padding;
std::atomic<ASEdgeInsets> _border;
std::atomic<CGFloat> _aspectRatio;
ASStackLayoutAlignItems _parentAlignStyle;
#endif
}
@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight;
@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize;
#pragma mark - Lifecycle
- (instancetype)initWithDelegate:(id<ASLayoutElementStyleDelegate>)delegate
{
self = [self init];
if (self) {
_delegate = delegate;
}
return self;
}
- (instancetype)init
{
self = [super init];
if (self) {
_size = ASLayoutElementSizeMake();
#if YOGA
_parentAlignStyle = ASStackLayoutAlignItemsNotSet;
#endif
}
return self;
}
ASSynthesizeLockingMethodsWithMutex(__instanceLock__)
#pragma mark - ASLayoutElementStyleSize
- (ASLayoutElementSize)size
{
return _size.load();
}
- (void)setSize:(ASLayoutElementSize)size
{
ASLayoutElementStyleSetSizeWithScope({
newSize = size;
});
// No CallDelegate method as ASLayoutElementSize is currently internal.
}
#pragma mark - ASLayoutElementStyleSizeForwarding
- (ASDimension)width
{
return _size.load().width;
}
- (void)setWidth:(ASDimension)width
{
ASLayoutElementStyleSetSizeWithScope({
newSize.width = width;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty);
}
- (ASDimension)height
{
return _size.load().height;
}
- (void)setHeight:(ASDimension)height
{
ASLayoutElementStyleSetSizeWithScope({
newSize.height = height;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty);
}
- (ASDimension)minWidth
{
return _size.load().minWidth;
}
- (void)setMinWidth:(ASDimension)minWidth
{
ASLayoutElementStyleSetSizeWithScope({
newSize.minWidth = minWidth;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty);
}
- (ASDimension)maxWidth
{
return _size.load().maxWidth;
}
- (void)setMaxWidth:(ASDimension)maxWidth
{
ASLayoutElementStyleSetSizeWithScope({
newSize.maxWidth = maxWidth;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty);
}
- (ASDimension)minHeight
{
return _size.load().minHeight;
}
- (void)setMinHeight:(ASDimension)minHeight
{
ASLayoutElementStyleSetSizeWithScope({
newSize.minHeight = minHeight;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty);
}
- (ASDimension)maxHeight
{
return _size.load().maxHeight;
}
- (void)setMaxHeight:(ASDimension)maxHeight
{
ASLayoutElementStyleSetSizeWithScope({
newSize.maxHeight = maxHeight;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty);
}
#pragma mark - ASLayoutElementStyleSizeHelpers
- (void)setPreferredSize:(CGSize)preferredSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.width = ASDimensionMakeWithPoints(preferredSize.width);
newSize.height = ASDimensionMakeWithPoints(preferredSize.height);
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty);
}
- (CGSize)preferredSize
{
ASLayoutElementSize size = _size.load();
if (size.width.unit == ASDimensionUnitFraction) {
NSCAssert(NO, @"Cannot get preferredSize of element with fractional width. Width: %@.", NSStringFromASDimension(size.width));
return CGSizeZero;
}
if (size.height.unit == ASDimensionUnitFraction) {
NSCAssert(NO, @"Cannot get preferredSize of element with fractional height. Height: %@.", NSStringFromASDimension(size.height));
return CGSizeZero;
}
return CGSizeMake(size.width.value, size.height.value);
}
- (void)setMinSize:(CGSize)minSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.minWidth = ASDimensionMakeWithPoints(minSize.width);
newSize.minHeight = ASDimensionMakeWithPoints(minSize.height);
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty);
}
- (void)setMaxSize:(CGSize)maxSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.maxWidth = ASDimensionMakeWithPoints(maxSize.width);
newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height);
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty);
}
- (ASLayoutSize)preferredLayoutSize
{
ASLayoutElementSize size = _size.load();
return ASLayoutSizeMake(size.width, size.height);
}
- (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.width = preferredLayoutSize.width;
newSize.height = preferredLayoutSize.height;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty);
}
- (ASLayoutSize)minLayoutSize
{
ASLayoutElementSize size = _size.load();
return ASLayoutSizeMake(size.minWidth, size.minHeight);
}
- (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.minWidth = minLayoutSize.width;
newSize.minHeight = minLayoutSize.height;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty);
}
- (ASLayoutSize)maxLayoutSize
{
ASLayoutElementSize size = _size.load();
return ASLayoutSizeMake(size.maxWidth, size.maxHeight);
}
- (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize
{
ASLayoutElementStyleSetSizeWithScope({
newSize.maxWidth = maxLayoutSize.width;
newSize.maxHeight = maxLayoutSize.height;
});
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty);
}
#pragma mark - ASStackLayoutElement
- (void)setSpacingBefore:(CGFloat)spacingBefore
{
_spacingBefore.store(spacingBefore);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty);
}
- (CGFloat)spacingBefore
{
return _spacingBefore.load();
}
- (void)setSpacingAfter:(CGFloat)spacingAfter
{
_spacingAfter.store(spacingAfter);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty);
}
- (CGFloat)spacingAfter
{
return _spacingAfter.load();
}
- (void)setFlexGrow:(CGFloat)flexGrow
{
_flexGrow.store(flexGrow);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty);
}
- (CGFloat)flexGrow
{
return _flexGrow.load();
}
- (void)setFlexShrink:(CGFloat)flexShrink
{
_flexShrink.store(flexShrink);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty);
}
- (CGFloat)flexShrink
{
return _flexShrink.load();
}
- (void)setFlexBasis:(ASDimension)flexBasis
{
_flexBasis.store(flexBasis);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty);
}
- (ASDimension)flexBasis
{
return _flexBasis.load();
}
- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf
{
_alignSelf.store(alignSelf);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty);
}
- (ASStackLayoutAlignSelf)alignSelf
{
return _alignSelf.load();
}
- (void)setAscender:(CGFloat)ascender
{
_ascender.store(ascender);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty);
}
- (CGFloat)ascender
{
return _ascender.load();
}
- (void)setDescender:(CGFloat)descender
{
_descender.store(descender);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty);
}
- (CGFloat)descender
{
return _descender.load();
}
#pragma mark - ASAbsoluteLayoutElement
- (void)setLayoutPosition:(CGPoint)layoutPosition
{
_layoutPosition.store(layoutPosition);
ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty);
}
- (CGPoint)layoutPosition
{
return _layoutPosition.load();
}
#pragma mark - Extensions
- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx
{
NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Setting index outside of max bool extensions space");
MutexLocker l(__instanceLock__);
_extensions.boolExtensions[idx] = value;
}
- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\
{
NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space");
MutexLocker l(__instanceLock__);
return _extensions.boolExtensions[idx];
}
- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx
{
NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Setting index outside of max integer extensions space");
MutexLocker l(__instanceLock__);
_extensions.integerExtensions[idx] = value;
}
- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx
{
NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space");
MutexLocker l(__instanceLock__);
return _extensions.integerExtensions[idx];
}
- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx
{
NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Setting index outside of max edge insets extensions space");
MutexLocker l(__instanceLock__);
_extensions.edgeInsetsExtensions[idx] = value;
}
- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx
{
NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space");
MutexLocker l(__instanceLock__);
return _extensions.edgeInsetsExtensions[idx];
}
#pragma mark - Debugging
- (NSString *)description
{
return ASObjectDescriptionMake(self, [self propertiesForDescription]);
}
- (NSMutableArray<NSDictionary *> *)propertiesForDescription
{
NSMutableArray<NSDictionary *> *result = [NSMutableArray array];
if ((self.minLayoutSize.width.unit != ASDimensionUnitAuto ||
self.minLayoutSize.height.unit != ASDimensionUnitAuto)) {
[result addObject:@{ @"minLayoutSize" : NSStringFromASLayoutSize(self.minLayoutSize) }];
}
if ((self.preferredLayoutSize.width.unit != ASDimensionUnitAuto ||
self.preferredLayoutSize.height.unit != ASDimensionUnitAuto)) {
[result addObject:@{ @"preferredSize" : NSStringFromASLayoutSize(self.preferredLayoutSize) }];
}
if ((self.maxLayoutSize.width.unit != ASDimensionUnitAuto ||
self.maxLayoutSize.height.unit != ASDimensionUnitAuto)) {
[result addObject:@{ @"maxLayoutSize" : NSStringFromASLayoutSize(self.maxLayoutSize) }];
}
if (self.alignSelf != ASStackLayoutAlignSelfAuto) {
[result addObject:@{ @"alignSelf" : [@[@"ASStackLayoutAlignSelfAuto",
@"ASStackLayoutAlignSelfStart",
@"ASStackLayoutAlignSelfEnd",
@"ASStackLayoutAlignSelfCenter",
@"ASStackLayoutAlignSelfStretch"] objectAtIndex:self.alignSelf] }];
}
if (self.ascender != 0) {
[result addObject:@{ @"ascender" : @(self.ascender) }];
}
if (self.descender != 0) {
[result addObject:@{ @"descender" : @(self.descender) }];
}
if (ASDimensionEqualToDimension(self.flexBasis, ASDimensionAuto) == NO) {
[result addObject:@{ @"flexBasis" : NSStringFromASDimension(self.flexBasis) }];
}
if (self.flexGrow != 0) {
[result addObject:@{ @"flexGrow" : @(self.flexGrow) }];
}
if (self.flexShrink != 0) {
[result addObject:@{ @"flexShrink" : @(self.flexShrink) }];
}
if (self.spacingAfter != 0) {
[result addObject:@{ @"spacingAfter" : @(self.spacingAfter) }];
}
if (self.spacingBefore != 0) {
[result addObject:@{ @"spacingBefore" : @(self.spacingBefore) }];
}
if (CGPointEqualToPoint(self.layoutPosition, CGPointZero) == NO) {
[result addObject:@{ @"layoutPosition" : [NSValue valueWithCGPoint:self.layoutPosition] }];
}
return result;
}
- (void)propertyDidChange:(NSString *)propertyName
{
#if YOGA
/* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT
void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow);
void YGNodeStyleSetFlex(YGNodeRef node, float flex);
*/
if (_yogaNode == NULL) {
return;
}
// Because the NSStrings used to identify each property are const, use efficient pointer comparison.
if (propertyName == ASLayoutElementStyleWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, self.width);
}
else if (propertyName == ASLayoutElementStyleMinWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, self.minWidth);
}
else if (propertyName == ASLayoutElementStyleMaxWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, self.maxWidth);
}
else if (propertyName == ASLayoutElementStyleHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, self.height);
}
else if (propertyName == ASLayoutElementStyleMinHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, self.minHeight);
}
else if (propertyName == ASLayoutElementStyleMaxHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, self.maxHeight);
}
else if (propertyName == ASLayoutElementStyleFlexGrowProperty) {
YGNodeStyleSetFlexGrow(_yogaNode, self.flexGrow);
}
else if (propertyName == ASLayoutElementStyleFlexShrinkProperty) {
YGNodeStyleSetFlexShrink(_yogaNode, self.flexShrink);
}
else if (propertyName == ASLayoutElementStyleFlexBasisProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, self.flexBasis);
}
else if (propertyName == ASLayoutElementStyleAlignSelfProperty) {
YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(self.alignSelf));
}
else if (propertyName == ASYogaFlexWrapProperty) {
YGNodeStyleSetFlexWrap(_yogaNode, self.flexWrap);
}
else if (propertyName == ASYogaFlexDirectionProperty) {
YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(self.flexDirection));
}
else if (propertyName == ASYogaDirectionProperty) {
YGNodeStyleSetDirection(_yogaNode, self.direction);
}
else if (propertyName == ASYogaJustifyContentProperty) {
YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(self.justifyContent));
}
else if (propertyName == ASYogaAlignItemsProperty) {
ASStackLayoutAlignItems alignItems = self.alignItems;
if (alignItems != ASStackLayoutAlignItemsNotSet) {
YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems));
}
}
else if (propertyName == ASYogaPositionTypeProperty) {
YGNodeStyleSetPositionType(_yogaNode, self.positionType);
}
else if (propertyName == ASYogaPositionProperty) {
ASEdgeInsets position = self.position;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaMarginProperty) {
ASEdgeInsets margin = self.margin;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaPaddingProperty) {
ASEdgeInsets padding = self.padding;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaBorderProperty) {
ASEdgeInsets border = self.border;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_FLOAT_WITH_EDGE(_yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaAspectRatioProperty) {
CGFloat aspectRatio = self.aspectRatio;
if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) {
YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio);
}
}
#endif
}
#pragma mark - Yoga Flexbox Properties
#if YOGA
+ (void)initialize
{
[super initialize];
YGConfigSetPointScaleFactor(YGConfigGetDefault(), ASScreenScale());
// Yoga recommends using Web Defaults for all new projects. This will be enabled for Texture very soon.
//YGConfigSetUseWebDefaults(YGConfigGetDefault(), true);
}
- (YGNodeRef)yogaNode
{
return _yogaNode;
}
- (YGNodeRef)yogaNodeCreateIfNeeded
{
if (_yogaNode == NULL) {
_yogaNode = YGNodeNew();
}
return _yogaNode;
}
- (void)destroyYogaNode
{
if (_yogaNode != NULL) {
// Release the __bridge_retained Context object.
ASLayoutElementYogaUpdateMeasureFunc(_yogaNode, nil);
YGNodeFree(_yogaNode);
_yogaNode = NULL;
}
}
- (void)dealloc
{
[self destroyYogaNode];
}
- (YGWrap)flexWrap { return _flexWrap.load(); }
- (ASStackLayoutDirection)flexDirection { return _flexDirection.load(); }
- (YGDirection)direction { return _direction.load(); }
- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); }
- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); }
- (YGPositionType)positionType { return _positionType.load(); }
- (ASEdgeInsets)position { return _position.load(); }
- (ASEdgeInsets)margin { return _margin.load(); }
- (ASEdgeInsets)padding { return _padding.load(); }
- (ASEdgeInsets)border { return _border.load(); }
- (CGFloat)aspectRatio { return _aspectRatio.load(); }
// private (ASLayoutElementStylePrivate.h)
- (ASStackLayoutAlignItems)parentAlignStyle {
return _parentAlignStyle;
}
- (void)setFlexWrap:(YGWrap)flexWrap {
_flexWrap.store(flexWrap);
ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty);
}
- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection {
_flexDirection.store(flexDirection);
ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty);
}
- (void)setDirection:(YGDirection)direction {
_direction.store(direction);
ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty);
}
- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify {
_justifyContent.store(justify);
ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty);
}
- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems {
_alignItems.store(alignItems);
ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty);
}
- (void)setPositionType:(YGPositionType)positionType {
_positionType.store(positionType);
ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty);
}
- (void)setPosition:(ASEdgeInsets)position {
_position.store(position);
ASLayoutElementStyleCallDelegate(ASYogaPositionProperty);
}
- (void)setMargin:(ASEdgeInsets)margin {
_margin.store(margin);
ASLayoutElementStyleCallDelegate(ASYogaMarginProperty);
}
- (void)setPadding:(ASEdgeInsets)padding {
_padding.store(padding);
ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty);
}
- (void)setBorder:(ASEdgeInsets)border {
_border.store(border);
ASLayoutElementStyleCallDelegate(ASYogaBorderProperty);
}
- (void)setAspectRatio:(CGFloat)aspectRatio {
_aspectRatio.store(aspectRatio);
ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty);
}
// private (ASLayoutElementStylePrivate.h)
- (void)setParentAlignStyle:(ASStackLayoutAlignItems)style {
_parentAlignStyle = style;
}
#endif /* YOGA */
@end

View File

@ -0,0 +1,31 @@
//
// ASLayoutElementStylePrivate.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#pragma once
#import <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
@interface ASLayoutElementStyle () <ASDescriptionProvider>
/**
* @abstract The object that acts as the delegate of the style.
*
* @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained.
*/
@property (nullable, nonatomic, weak) id<ASLayoutElementStyleDelegate> delegate;
/**
* @abstract A size constraint that should apply to this ASLayoutElement.
*/
@property (nonatomic, readonly) ASLayoutElementSize size;
@property (nonatomic, assign) ASStackLayoutAlignItems parentAlignStyle;
@end

View File

@ -0,0 +1,16 @@
//
// ASLayoutManager.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
AS_SUBCLASSING_RESTRICTED
@interface ASLayoutManager : NSLayoutManager
@end

View File

@ -0,0 +1,42 @@
//
// ASLayoutManager.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASLayoutManager.h"
@implementation ASLayoutManager
- (void)showCGGlyphs:(const CGGlyph *)glyphs
positions:(const CGPoint *)positions
count:(NSUInteger)glyphCount
font:(UIFont *)font
matrix:(CGAffineTransform)textMatrix
attributes:(NSDictionary *)attributes
inContext:(CGContextRef)graphicsContext
{
// NSLayoutManager has a hard coded internal color for hyperlinks which ignores
// NSForegroundColorAttributeName. To get around this, we force the fill color
// in the current context to match NSForegroundColorAttributeName.
UIColor *foregroundColor = attributes[NSForegroundColorAttributeName];
if (foregroundColor)
{
CGContextSetFillColorWithColor(graphicsContext, foregroundColor.CGColor);
}
[super showCGGlyphs:glyphs
positions:positions
count:glyphCount
font:font
matrix:textMatrix
attributes:attributes
inContext:graphicsContext];
}
@end

View File

@ -0,0 +1,59 @@
//
// ASLayoutSpec+Subclasses.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import <AsyncDisplayKit/ASLayout.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ASLayoutElement;
@interface ASLayoutSpec (Subclassing)
/**
* Adds a child with the given identifier to this layout spec.
*
* @param child A child to be added.
*
* @param index An index associated with the child.
*
* @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the
* responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec,
* only require a single child.
*
* For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example)
* a subclass can use the setChild method to set the "primary" child. It should then use this method
* to set any other required children. Ideally a subclass would hide this from the user, and use the
* setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild
* property that behind the scenes is calling setChild:forIndex:.
*/
- (void)setChild:(id<ASLayoutElement>)child atIndex:(NSUInteger)index;
/**
* Returns the child added to this layout spec using the given index.
*
* @param index An identifier associated with the the child.
*/
- (nullable id<ASLayoutElement>)childAtIndex:(NSUInteger)index;
@end
@interface ASLayout ()
/**
* Position in parent. Default to CGPointNull.
*
* @discussion When being used as a sublayout, this property must not equal CGPointNull.
*/
@property (nonatomic) CGPoint position;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,87 @@
//
// ASLayoutSpec+Subclasses.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import "ASLayoutSpecPrivate.h"
#pragma mark - ASNullLayoutSpec
@interface ASNullLayoutSpec : ASLayoutSpec
- (instancetype)init NS_UNAVAILABLE;
+ (ASNullLayoutSpec *)null;
@end
@implementation ASNullLayoutSpec : ASLayoutSpec
+ (ASNullLayoutSpec *)null
{
static ASNullLayoutSpec *sharedNullLayoutSpec = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedNullLayoutSpec = [[self alloc] init];
});
return sharedNullLayoutSpec;
}
- (BOOL)isMutable
{
return NO;
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
return [ASLayout layoutWithLayoutElement:self size:CGSizeZero];
}
@end
#pragma mark - ASLayoutSpec (Subclassing)
@implementation ASLayoutSpec (Subclassing)
#pragma mark - Child with index
- (void)setChild:(id<ASLayoutElement>)child atIndex:(NSUInteger)index
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
id<ASLayoutElement> layoutElement = child ?: [ASNullLayoutSpec null];
if (child) {
if (_childrenArray.count < index) {
// Fill up the array with null objects until the index
NSInteger i = _childrenArray.count;
while (i < index) {
_childrenArray[i] = [ASNullLayoutSpec null];
i++;
}
}
}
// Replace object at the given index with the layoutElement
_childrenArray[index] = layoutElement;
}
- (id<ASLayoutElement>)childAtIndex:(NSUInteger)index
{
id<ASLayoutElement> layoutElement = nil;
if (index < _childrenArray.count) {
layoutElement = _childrenArray[index];
}
// Null layoutElement should not be accessed
ASDisplayNodeAssert(layoutElement != [ASNullLayoutSpec null], @"Access child at index without set a child at that index");
return layoutElement;
}
@end

View File

@ -0,0 +1,338 @@
//
// ASLayoutSpec.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import "ASLayoutSpecPrivate.h"
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASCollections.h>
#import "ASLayoutElementStylePrivate.h"
#import <AsyncDisplayKit/ASTraitCollection.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <objc/runtime.h>
#import <map>
#import <vector>
@implementation ASLayoutSpec
// Dynamic properties for ASLayoutElements
@dynamic layoutElementType;
@synthesize debugName = _debugName;
#pragma mark - Lifecycle
- (instancetype)init
{
if (!(self = [super init])) {
return nil;
}
_isMutable = YES;
_primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault();
_childrenArray = [[NSMutableArray alloc] init];
return self;
}
- (ASLayoutElementType)layoutElementType
{
return ASLayoutElementTypeLayoutSpec;
}
- (BOOL)canLayoutAsynchronous
{
return YES;
}
- (BOOL)implementsLayoutMethod
{
return YES;
}
#pragma mark - Style
- (ASLayoutElementStyle *)style
{
AS::MutexLocker l(__instanceLock__);
if (_style == nil) {
_style = [[ASLayoutElementStyle alloc] init];
}
return _style;
}
- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock
{
styleBlock(self.style);
return self;
}
#pragma mark - Layout
ASLayoutElementLayoutCalculationDefaults
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
return [ASLayout layoutWithLayoutElement:self size:constrainedSize.min];
}
#pragma mark - Child
- (void)setChild:(id<ASLayoutElement>)child
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API");
if (child) {
_childrenArray[0] = child;
} else {
if (_childrenArray.count) {
[_childrenArray removeObjectAtIndex:0];
}
}
}
- (id<ASLayoutElement>)child
{
ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API");
return _childrenArray.firstObject;
}
#pragma mark - Children
- (void)setChildren:(NSArray<id<ASLayoutElement>> *)children
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
for (id<ASLayoutElement> child in children) {
ASDisplayNodeAssert([child conformsToProtocol:NSProtocolFromString(@"ASLayoutElement")], @"Child %@ of spec %@ is not an ASLayoutElement!", child, self);
}
#endif
[_childrenArray setArray:children];
}
- (nullable NSArray<id<ASLayoutElement>> *)children
{
return [_childrenArray copy];
}
- (NSArray<id<ASLayoutElement>> *)sublayoutElements
{
return [_childrenArray copy];
}
#pragma mark - NSFastEnumeration
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len
{
return [_childrenArray countByEnumeratingWithState:state objects:buffer count:len];
}
#pragma mark - ASTraitEnvironment
- (ASTraitCollection *)asyncTraitCollection
{
AS::MutexLocker l(__instanceLock__);
return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection];
}
ASPrimitiveTraitCollectionDefaults
#pragma mark - ASLayoutElementStyleExtensibility
ASLayoutElementStyleExtensibilityForwarding
#pragma mark - ASDescriptionProvider
- (NSMutableArray<NSDictionary *> *)propertiesForDescription
{
const auto result = [NSMutableArray<NSDictionary *> array];
if (NSArray *children = self.children) {
// Use tiny descriptions because these trees can get nested very deep.
const auto tinyDescriptions = ASArrayByFlatMapping(children, id object, ASObjectDescriptionMakeTiny(object));
[result addObject:@{ @"children": tinyDescriptions }];
}
return result;
}
- (NSString *)description
{
return ASObjectDescriptionMake(self, [self propertiesForDescription]);
}
#pragma mark - Framework Private
#if AS_DEDUPE_LAYOUT_SPEC_TREE
- (nullable NSHashTable<id<ASLayoutElement>> *)findDuplicatedElementsInSubtree
{
NSHashTable *result = nil;
NSUInteger count = 0;
[self _findDuplicatedElementsInSubtreeWithWorkingSet:[NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality] workingCount:&count result:&result];
return result;
}
/**
* This method is extremely performance-sensitive, so we do some strange things.
*
* @param workingSet A working set of elements for use in the recursion.
* @param workingCount The current count of the set for use in the recursion.
* @param result The set into which to put the result. This initially points to @c nil to save time if no duplicates exist.
*/
- (void)_findDuplicatedElementsInSubtreeWithWorkingSet:(NSHashTable<id<ASLayoutElement>> *)workingSet workingCount:(NSUInteger *)workingCount result:(NSHashTable<id<ASLayoutElement>> * _Nullable *)result
{
Class layoutSpecClass = [ASLayoutSpec class];
for (id<ASLayoutElement> child in self) {
// Add the object into the set.
[workingSet addObject:child];
// Check that addObject: caused the count to increase.
// This is faster than using containsObject.
NSUInteger oldCount = *workingCount;
NSUInteger newCount = workingSet.count;
BOOL objectAlreadyExisted = (newCount != oldCount + 1);
if (objectAlreadyExisted) {
if (*result == nil) {
*result = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
}
[*result addObject:child];
} else {
*workingCount = newCount;
// If child is a layout spec we haven't visited, recurse its children.
if ([child isKindOfClass:layoutSpecClass]) {
[(ASLayoutSpec *)child _findDuplicatedElementsInSubtreeWithWorkingSet:workingSet workingCount:workingCount result:result];
}
}
}
}
#endif
#pragma mark - Debugging
- (NSString *)debugName
{
AS::MutexLocker l(__instanceLock__);
return _debugName;
}
- (void)setDebugName:(NSString *)debugName
{
AS::MutexLocker l(__instanceLock__);
if (!ASObjectIsEqual(_debugName, debugName)) {
_debugName = [debugName copy];
}
}
#pragma mark - ASLayoutElementAsciiArtProtocol
- (NSString *)asciiArtString
{
NSArray *children = self.children.count < 2 && self.child ? @[self.child] : self.children;
return [ASLayoutSpec asciiArtStringForChildren:children parentName:[self asciiArtName]];
}
- (NSString *)asciiArtName
{
NSMutableString *result = [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding];
if (_debugName) {
[result appendFormat:@" (%@)", _debugName];
}
return result;
}
ASSynthesizeLockingMethodsWithMutex(__instanceLock__)
@end
#pragma mark - ASWrapperLayoutSpec
@implementation ASWrapperLayoutSpec
+ (instancetype)wrapperWithLayoutElement:(id<ASLayoutElement>)layoutElement NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElement:layoutElement];
}
- (instancetype)initWithLayoutElement:(id<ASLayoutElement>)layoutElement
{
self = [super init];
if (self) {
self.child = layoutElement;
}
return self;
}
+ (instancetype)wrapperWithLayoutElements:(NSArray<id<ASLayoutElement>> *)layoutElements NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElements:layoutElements];
}
- (instancetype)initWithLayoutElements:(NSArray<id<ASLayoutElement>> *)layoutElements
{
self = [super init];
if (self) {
self.children = layoutElements;
}
return self;
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
NSArray *children = self.children;
const auto count = children.count;
ASLayout *rawSublayouts[count];
int i = 0;
CGSize size = constrainedSize.min;
for (id<ASLayoutElement> child in children) {
ASLayout *sublayout = [child layoutThatFits:constrainedSize parentSize:constrainedSize.max];
sublayout.position = CGPointZero;
size.width = MAX(size.width, sublayout.size.width);
size.height = MAX(size.height, sublayout.size.height);
rawSublayouts[i++] = sublayout;
}
const auto sublayouts = [NSArray<ASLayout *> arrayByTransferring:rawSublayouts count:i];
return [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts];
}
@end
#pragma mark - ASLayoutSpec (Debugging)
@implementation ASLayoutSpec (Debugging)
#pragma mark - ASCII Art Helpers
+ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction
{
NSMutableArray *childStrings = [NSMutableArray array];
for (id<ASLayoutElementAsciiArtProtocol> layoutChild in children) {
NSString *childString = [layoutChild asciiArtString];
if (childString) {
[childStrings addObject:childString];
}
}
if (direction == ASStackLayoutDirectionHorizontal) {
return [ASAsciiArtBoxCreator horizontalBoxStringForChildren:childStrings parent:parentName];
}
return [ASAsciiArtBoxCreator verticalBoxStringForChildren:childStrings parent:parentName];
}
+ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName
{
return [self asciiArtStringForChildren:children parentName:parentName direction:ASStackLayoutDirectionHorizontal];
}
@end

View File

@ -0,0 +1,37 @@
//
// ASLayoutSpecPrivate.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASThread.h>
#if DEBUG
#define AS_DEDUPE_LAYOUT_SPEC_TREE 1
#else
#define AS_DEDUPE_LAYOUT_SPEC_TREE 0
#endif
NS_ASSUME_NONNULL_BEGIN
@interface ASLayoutSpec() {
AS::RecursiveMutex __instanceLock__;
std::atomic <ASPrimitiveTraitCollection> _primitiveTraitCollection;
ASLayoutElementStyle *_style;
NSMutableArray *_childrenArray;
}
#if AS_DEDUPE_LAYOUT_SPEC_TREE
/**
* Recursively search the subtree for elements that occur more than once.
*/
- (nullable NSHashTable<id<ASLayoutElement>> *)findDuplicatedElementsInSubtree;
#endif
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,103 @@
//
// ASLayoutSpecUtilities.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <CoreGraphics/CoreGraphics.h>
#import <algorithm>
#import <functional>
#import <type_traits>
#import <vector>
namespace AS {
// adopted from http://stackoverflow.com/questions/14945223/map-function-with-c11-constructs
// Takes an iterable, applies a function to every element,
// and returns a vector of the results
//
template <typename T, typename Func>
auto map(const T &iterable, Func &&func) -> std::vector<decltype(func(std::declval<typename T::value_type>()))>
{
// Some convenience type definitions
typedef decltype(func(std::declval<typename T::value_type>())) value_type;
typedef std::vector<value_type> result_type;
// Prepares an output vector of the appropriate size
result_type res(iterable.size());
// Let std::transform apply `func` to all elements
// (use perfect forwarding for the function object)
std::transform(
begin(iterable), end(iterable), res.begin(),
std::forward<Func>(func)
);
return res;
}
template<typename Func>
auto map(id<NSFastEnumeration> collection, Func &&func) -> std::vector<decltype(func(std::declval<id>()))>
{
std::vector<decltype(func(std::declval<id>()))> to;
for (id obj in collection) {
to.push_back(func(obj));
}
return to;
}
template <typename T, typename Func>
auto filter(const T &iterable, Func &&func) -> std::vector<typename T::value_type>
{
std::vector<typename T::value_type> to;
for (auto obj : iterable) {
if (func(obj)) {
to.push_back(obj);
}
}
return to;
}
};
inline CGPoint operator+(const CGPoint &p1, const CGPoint &p2)
{
return { p1.x + p2.x, p1.y + p2.y };
}
inline CGPoint operator-(const CGPoint &p1, const CGPoint &p2)
{
return { p1.x - p2.x, p1.y - p2.y };
}
inline CGSize operator+(const CGSize &s1, const CGSize &s2)
{
return { s1.width + s2.width, s1.height + s2.height };
}
inline CGSize operator-(const CGSize &s1, const CGSize &s2)
{
return { s1.width - s2.width, s1.height - s2.height };
}
inline UIEdgeInsets operator+(const UIEdgeInsets &e1, const UIEdgeInsets &e2)
{
return { e1.top + e2.top, e1.left + e2.left, e1.bottom + e2.bottom, e1.right + e2.right };
}
inline UIEdgeInsets operator-(const UIEdgeInsets &e1, const UIEdgeInsets &e2)
{
return { e1.top - e2.top, e1.left - e2.left, e1.bottom - e2.bottom, e1.right - e2.right };
}
inline UIEdgeInsets operator*(const UIEdgeInsets &e1, const UIEdgeInsets &e2)
{
return { e1.top * e2.top, e1.left * e2.left, e1.bottom * e2.bottom, e1.right * e2.right };
}
inline UIEdgeInsets operator-(const UIEdgeInsets &e)
{
return { -e.top, -e.left, -e.bottom, -e.right };
}

View File

@ -0,0 +1,94 @@
//
// ASLayoutTransition.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/_ASTransitionContext.h>
#import "ASDisplayNodeLayout.h"
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - ASLayoutElementTransition
/**
* Objects conform to this project returns if it's possible to layout asynchronous
*/
@protocol ASLayoutElementTransition <NSObject>
/**
* @abstract Returns if the layoutElement can be used to layout in an asynchronous way on a background thread.
*/
@property (nonatomic, readonly) BOOL canLayoutAsynchronous;
@end
@interface ASDisplayNode () <ASLayoutElementTransition>
@end
@interface ASLayoutSpec () <ASLayoutElementTransition>
@end
#pragma mark - ASLayoutTransition
AS_SUBCLASSING_RESTRICTED
@interface ASLayoutTransition : NSObject <_ASTransitionContextLayoutDelegate>
/**
* Node to apply layout transition on
*/
@property (nonatomic, weak, readonly) ASDisplayNode *node;
/**
* Previous layout to transition from
*/
@property (nonatomic, readonly) const ASDisplayNodeLayout &previousLayout NS_RETURNS_INNER_POINTER;
/**
* Pending layout to transition to
*/
@property (nonatomic, readonly) const ASDisplayNodeLayout &pendingLayout NS_RETURNS_INNER_POINTER;
/**
* Returns if the layout transition needs to happen synchronously
*/
@property (nonatomic, readonly) BOOL isSynchronous;
/**
* Returns a newly initialized layout transition
*/
- (instancetype)initWithNode:(ASDisplayNode *)node
pendingLayout:(const ASDisplayNodeLayout &)pendingLayout
previousLayout:(const ASDisplayNodeLayout &)previousLayout NS_DESIGNATED_INITIALIZER;
/**
* Insert and remove subnodes that were added or removed between the previousLayout and the pendingLayout
*/
- (void)commitTransition;
/**
* Insert all new subnodes that were added and move the subnodes that moved between the previous layout and
* the pending layout.
*/
- (void)applySubnodeInsertionsAndMoves;
/**
* Remove all subnodes that are removed between the previous layout and the pending layout
*/
- (void)applySubnodeRemovals;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)new NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,298 @@
//
// ASLayoutTransition.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASLayoutTransition.h"
#import <AsyncDisplayKit/NSArray+Diffing.h>
#import <AsyncDisplayKit/ASLayout.h>
#import "ASDisplayNodeInternal.h" // Required for _insertSubnode... / _removeFromSupernode.
#import <queue>
#if AS_IG_LIST_KIT
#import <IGListKit/IGListKit.h>
#import <AsyncDisplayKit/ASLayout+IGListKit.h>
#endif
using AS::MutexLocker;
/**
* Search the whole layout stack if at least one layout has a layoutElement object that can not be layed out asynchronous.
* This can be the case for example if a node was already loaded
*/
static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
// Queue used to keep track of sublayouts while traversing this layout in a BFS fashion.
std::queue<ASLayout *> queue;
queue.push(layout);
while (!queue.empty()) {
layout = queue.front();
queue.pop();
#if DEBUG
ASDisplayNodeCAssert([layout.layoutElement conformsToProtocol:@protocol(ASLayoutElementTransition)], @"ASLayoutElement in a layout transition needs to conforms to the ASLayoutElementTransition protocol.");
#endif
if (((id<ASLayoutElementTransition>)layout.layoutElement).canLayoutAsynchronous == NO) {
return NO;
}
// Add all sublayouts to process in next step
for (ASLayout *sublayout in layout.sublayouts) {
queue.push(sublayout);
}
}
return YES;
}
@implementation ASLayoutTransition {
std::shared_ptr<AS::RecursiveMutex> __instanceLock__;
BOOL _calculatedSubnodeOperations;
NSArray<ASDisplayNode *> *_insertedSubnodes;
NSArray<ASDisplayNode *> *_removedSubnodes;
std::vector<NSUInteger> _insertedSubnodePositions;
std::vector<std::pair<ASDisplayNode *, NSUInteger>> _subnodeMoves;
ASDisplayNodeLayout _pendingLayout;
ASDisplayNodeLayout _previousLayout;
}
- (instancetype)initWithNode:(ASDisplayNode *)node
pendingLayout:(const ASDisplayNodeLayout &)pendingLayout
previousLayout:(const ASDisplayNodeLayout &)previousLayout
{
self = [super init];
if (self) {
__instanceLock__ = std::make_shared<AS::RecursiveMutex>();
_node = node;
_pendingLayout = pendingLayout;
_previousLayout = previousLayout;
}
return self;
}
- (BOOL)isSynchronous
{
MutexLocker l(*__instanceLock__);
return !ASLayoutCanTransitionAsynchronous(_pendingLayout.layout);
}
- (void)commitTransition
{
[self applySubnodeRemovals];
[self applySubnodeInsertionsAndMoves];
}
- (void)applySubnodeInsertionsAndMoves
{
MutexLocker l(*__instanceLock__);
[self calculateSubnodeOperationsIfNeeded];
// Create an activity even if no subnodes affected.
if (_insertedSubnodePositions.size() == 0 && _subnodeMoves.size() == 0) {
return;
}
ASDisplayNodeLogEvent(_node, @"insertSubnodes: %@", _insertedSubnodes);
NSUInteger i = 0;
NSUInteger j = 0;
for (auto const &move : _subnodeMoves) {
[move.first _removeFromSupernodeIfEqualTo:_node];
}
j = 0;
while (i < _insertedSubnodePositions.size() && j < _subnodeMoves.size()) {
NSUInteger p = _insertedSubnodePositions[i];
NSUInteger q = _subnodeMoves[j].second;
if (p < q) {
[_node _insertSubnode:_insertedSubnodes[i] atIndex:p];
i++;
} else {
[_node _insertSubnode:_subnodeMoves[j].first atIndex:q];
j++;
}
}
for (; i < _insertedSubnodePositions.size(); ++i) {
[_node _insertSubnode:_insertedSubnodes[i] atIndex:_insertedSubnodePositions[i]];
}
for (; j < _subnodeMoves.size(); ++j) {
[_node _insertSubnode:_subnodeMoves[j].first atIndex:_subnodeMoves[j].second];
}
}
- (void)applySubnodeRemovals
{
MutexLocker l(*__instanceLock__);
[self calculateSubnodeOperationsIfNeeded];
if (_removedSubnodes.count == 0) {
return;
}
ASDisplayNodeLogEvent(_node, @"removeSubnodes: %@", _removedSubnodes);
for (ASDisplayNode *subnode in _removedSubnodes) {
// In this case we should only remove the subnode if it's still a subnode of the _node that executes a layout transition.
// It can happen that a node already did a layout transition and added this subnode, in this case the subnode
// would be removed from the new node instead of _node
if (_node.automaticallyManagesSubnodes) {
[subnode _removeFromSupernodeIfEqualTo:_node];
}
}
}
- (void)calculateSubnodeOperationsIfNeeded
{
MutexLocker l(*__instanceLock__);
if (_calculatedSubnodeOperations) {
return;
}
// Create an activity even if no subnodes affected.
ASLayout *previousLayout = _previousLayout.layout;
ASLayout *pendingLayout = _pendingLayout.layout;
if (previousLayout) {
#if AS_IG_LIST_KIT
// IGListDiff completes in linear time O(m+n), so use it if we have it:
IGListIndexSetResult *result = IGListDiff(previousLayout.sublayouts, pendingLayout.sublayouts, IGListDiffEquality);
_insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, result.inserts, &_insertedSubnodes);
findNodesInLayoutAtIndexes(previousLayout, result.deletes, &_removedSubnodes);
for (IGListMoveIndex *move in result.moves) {
_subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[move.from].layoutElement, move.to));
}
// Sort by ascending order of move destinations, this will allow easy loop of `insertSubnode:AtIndex` later.
std::sort(_subnodeMoves.begin(), _subnodeMoves.end(), [](std::pair<id<ASLayoutElement>, NSUInteger> a,
std::pair<ASDisplayNode *, NSUInteger> b) {
return a.second < b.second;
});
#else
NSIndexSet *insertions, *deletions;
NSArray<NSIndexPath *> *moves;
NSArray<ASDisplayNode *> *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"];
NSArray<ASDisplayNode *> *pendingNodes = [pendingLayout.sublayouts valueForKey:@"layoutElement"];
[previousNodes asdk_diffWithArray:pendingNodes
insertions:&insertions
deletions:&deletions
moves:&moves];
_insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, insertions, &_insertedSubnodes);
_removedSubnodes = [previousNodes objectsAtIndexes:deletions];
// These should arrive sorted in ascending order of move destinations.
for (NSIndexPath *move in moves) {
_subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[([move indexAtPosition:0])].layoutElement,
[move indexAtPosition:1]));
}
#endif
} else {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pendingLayout.sublayouts count])];
_insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, indexes, &_insertedSubnodes);
_removedSubnodes = nil;
}
_calculatedSubnodeOperations = YES;
}
#pragma mark - _ASTransitionContextDelegate
- (NSArray<ASDisplayNode *> *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
MutexLocker l(*__instanceLock__);
return _node.subnodes;
}
- (NSArray<ASDisplayNode *> *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
MutexLocker l(*__instanceLock__);
[self calculateSubnodeOperationsIfNeeded];
return _insertedSubnodes;
}
- (NSArray<ASDisplayNode *> *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
MutexLocker l(*__instanceLock__);
[self calculateSubnodeOperationsIfNeeded];
return _removedSubnodes;
}
- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key
{
MutexLocker l(*__instanceLock__);
if ([key isEqualToString:ASTransitionContextFromLayoutKey]) {
return _previousLayout.layout;
} else if ([key isEqualToString:ASTransitionContextToLayoutKey]) {
return _pendingLayout.layout;
} else {
return nil;
}
}
- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key
{
MutexLocker l(*__instanceLock__);
if ([key isEqualToString:ASTransitionContextFromLayoutKey]) {
return _previousLayout.constrainedSize;
} else if ([key isEqualToString:ASTransitionContextToLayoutKey]) {
return _pendingLayout.constrainedSize;
} else {
return ASSizeRangeMake(CGSizeZero, CGSizeZero);
}
}
#pragma mark - Filter helpers
/**
* @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector.
*/
static inline std::vector<NSUInteger> findNodesInLayoutAtIndexes(ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> * __strong *storedNodes)
{
return findNodesInLayoutAtIndexesWithFilteredNodes(layout, indexes, nil, storedNodes);
}
/**
* @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector.
* Call only with a flattened layout.
* @discussion If the node exists in the `filteredNodes` array, the node is not added to `storedNodes`.
*/
static inline std::vector<NSUInteger> findNodesInLayoutAtIndexesWithFilteredNodes(ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> *filteredNodes,
NSArray<ASDisplayNode *> * __strong *storedNodes)
{
NSMutableArray<ASDisplayNode *> *nodes = [NSMutableArray arrayWithCapacity:indexes.count];
std::vector<NSUInteger> positions = std::vector<NSUInteger>();
// From inspection, this is how enumerateObjectsAtIndexes: works under the hood
NSUInteger firstIndex = indexes.firstIndex;
NSUInteger lastIndex = indexes.lastIndex;
NSUInteger idx = 0;
for (ASLayout *sublayout in layout.sublayouts) {
if (idx > lastIndex) { break; }
if (idx >= firstIndex && [indexes containsIndex:idx]) {
ASDisplayNode *node = (ASDisplayNode *)(sublayout.layoutElement);
ASDisplayNodeCAssert(node, @"ASDisplayNode was deallocated before it was added to a subnode. It's likely the case that you use automatically manages subnodes and allocate a ASDisplayNode in layoutSpecThatFits: and don't have any strong reference to it.");
ASDisplayNodeCAssert([node isKindOfClass:[ASDisplayNode class]], @"sublayout is an ASLayout, but not an ASDisplayNode - only call findNodesInLayoutAtIndexesWithFilteredNodes with a flattened layout (all sublayouts are ASDisplayNodes).");
if (node != nil) {
BOOL notFiltered = (filteredNodes == nil || [filteredNodes indexOfObjectIdenticalTo:node] == NSNotFound);
if (notFiltered) {
[nodes addObject:node];
positions.push_back(idx);
}
}
}
idx += 1;
}
*storedNodes = nodes;
return positions;
}
@end

View File

@ -0,0 +1,19 @@
//
// ASMainSerialQueue.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
AS_SUBCLASSING_RESTRICTED
@interface ASMainSerialQueue : NSObject
@property (nonatomic, readonly) NSUInteger numberOfScheduledBlocks;
- (void)performBlockOnMainThread:(dispatch_block_t)block;
@end

View File

@ -0,0 +1,81 @@
//
// ASMainSerialQueue.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASMainSerialQueue.h"
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
@interface ASMainSerialQueue ()
{
AS::Mutex _serialQueueLock;
NSMutableArray *_blocks;
}
@end
@implementation ASMainSerialQueue
- (instancetype)init
{
if (!(self = [super init])) {
return nil;
}
_blocks = [[NSMutableArray alloc] init];
return self;
}
- (NSUInteger)numberOfScheduledBlocks
{
AS::MutexLocker l(_serialQueueLock);
return _blocks.count;
}
- (void)performBlockOnMainThread:(dispatch_block_t)block
{
AS::UniqueLock l(_serialQueueLock);
[_blocks addObject:block];
{
l.unlock();
[self runBlocks];
l.lock();
}
}
- (void)runBlocks
{
dispatch_block_t mainThread = ^{
AS::UniqueLock l(self->_serialQueueLock);
do {
dispatch_block_t block;
if (self->_blocks.count > 0) {
block = _blocks[0];
[self->_blocks removeObjectAtIndex:0];
} else {
break;
}
{
l.unlock();
block();
l.lock();
}
} while (true);
};
ASPerformBlockOnMainThread(mainThread);
}
- (NSString *)description
{
return [[super description] stringByAppendingFormat:@" Blocks: %@", _blocks];
}
@end

View File

@ -0,0 +1,199 @@
//
// ASMainThreadDeallocation.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASThread.h>
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
@implementation NSObject (ASMainThreadIvarTeardown)
- (void)scheduleIvarsForMainThreadDeallocation
{
if (ASDisplayNodeThreadIsMain()) {
return;
}
NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation];
// Unwrap the ivar array
unsigned int count = 0;
// Will be unused if assertions are disabled.
__unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count);
ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType);
Ivar ivars[count];
[ivarsObj getValue:ivars];
for (Ivar ivar : ivars) {
id value = object_getIvar(self, ivar);
if (value == nil) {
continue;
}
if ([object_getClass(value) needsMainThreadDeallocation]) {
// Release the ivar's reference before handing the object to the queue so we
// don't risk holding onto it longer than the queue does.
object_setIvar(self, ivar, nil);
ASPerformMainThreadDeallocation(&value);
} else {
}
}
}
/**
* Returns an NSValue-wrapped array of all the ivars in this class or its superclasses
* up through ASDisplayNode, that we expect may need to be deallocated on main.
*
* This method caches its results.
*
* Result is of type NSValue<[Ivar]>
*/
+ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation NS_RETURNS_RETAINED
{
static NSCache<Class, NSValue *> *ivarsCache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ivarsCache = [[NSCache alloc] init];
});
NSValue *result = [ivarsCache objectForKey:self];
if (result != nil) {
return result;
}
// Cache miss.
unsigned int resultCount = 0;
static const int kMaxDealloc2MainIvarsPerClassTree = 64;
Ivar resultIvars[kMaxDealloc2MainIvarsPerClassTree];
// Get superclass results first.
Class c = class_getSuperclass(self);
if (c != [NSObject class]) {
NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation];
// Unwrap the ivar array and append it to our working array
unsigned int count = 0;
// Will be unused if assertions are disabled.
__unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count);
ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType);
ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count);
[ivarsObj getValue:resultIvars + resultCount];
resultCount += count;
}
// Now gather ivars from this particular class.
unsigned int allMyIvarsCount;
Ivar *allMyIvars = class_copyIvarList(self, &allMyIvarsCount);
for (NSUInteger i = 0; i < allMyIvarsCount; i++) {
Ivar ivar = allMyIvars[i];
// NOTE: Would be great to exclude weak/unowned ivars, since we don't
// release them. Unfortunately the objc_ivar_management access is private and
// class_getWeakIvarLayout does not have a well-defined structure.
const char *type = ivar_getTypeEncoding(ivar);
if (type != NULL && strcmp(type, @encode(id)) == 0) {
// If it's `id` we have to include it just in case.
resultIvars[resultCount] = ivar;
resultCount += 1;
} else {
// If it's an ivar with a static type, check the type.
Class c = ASGetClassFromType(type);
if ([c needsMainThreadDeallocation]) {
resultIvars[resultCount] = ivar;
resultCount += 1;
} else {
}
}
}
free(allMyIvars);
// Encode the type (array of Ivars) into a string and wrap it in an NSValue
char arrayType[32];
snprintf(arrayType, 32, "[%u^{objc_ivar}]", resultCount);
result = [NSValue valueWithBytes:resultIvars objCType:arrayType];
[ivarsCache setObject:result forKey:self];
return result;
}
@end
@implementation NSObject (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
const auto name = class_getName(self);
if (0 == strncmp(name, "AV", 2) || 0 == strncmp(name, "UI", 2) || 0 == strncmp(name, "CA", 2)) {
return YES;
}
return NO;
}
@end
@implementation CALayer (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return YES;
}
@end
@implementation UIColor (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return NO;
}
@end
@implementation UIGestureRecognizer (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return YES;
}
@end
@implementation UIImage (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return NO;
}
@end
@implementation UIResponder (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return YES;
}
@end
@implementation NSProxy (ASNeedsMainThreadDeallocation)
+ (BOOL)needsMainThreadDeallocation
{
return NO;
}
@end

View File

@ -0,0 +1,101 @@
//
// ASObjectDescriptionHelpers.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <UIKit/UIGeometry.h>
#import "NSIndexSet+ASHelpers.h"
NSString *ASGetDescriptionValueString(id object)
{
if ([object isKindOfClass:[NSValue class]]) {
// Use shortened NSValue descriptions
NSValue *value = object;
const char *type = value.objCType;
if (strcmp(type, @encode(CGRect)) == 0) {
CGRect rect = [value CGRectValue];
return [NSString stringWithFormat:@"(%g %g; %g %g)", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height];
} else if (strcmp(type, @encode(CGSize)) == 0) {
return NSStringFromCGSize(value.CGSizeValue);
} else if (strcmp(type, @encode(CGPoint)) == 0) {
return NSStringFromCGPoint(value.CGPointValue);
}
} else if ([object isKindOfClass:[NSIndexSet class]]) {
return [object as_smallDescription];
} else if ([object isKindOfClass:[NSIndexPath class]]) {
// index paths like (0, 7)
NSIndexPath *indexPath = object;
NSMutableArray *strings = [NSMutableArray array];
for (NSUInteger i = 0; i < indexPath.length; i++) {
[strings addObject:[NSString stringWithFormat:@"%lu", (unsigned long)[indexPath indexAtPosition:i]]];
}
return [NSString stringWithFormat:@"(%@)", [strings componentsJoinedByString:@", "]];
} else if ([object respondsToSelector:@selector(componentsJoinedByString:)]) {
// e.g. "[ <MYObject: 0x00000000> <MYObject: 0xFFFFFFFF> ]"
return [NSString stringWithFormat:@"[ %@ ]", [object componentsJoinedByString:@" "]];
}
return [object description];
}
NSString *_ASObjectDescriptionMakePropertyList(NSArray<NSDictionary *> * _Nullable propertyGroups)
{
NSMutableArray *components = [NSMutableArray array];
for (NSDictionary *properties in propertyGroups) {
[properties enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString *str;
if (key == (id)kCFNull) {
str = ASGetDescriptionValueString(obj);
} else {
str = [NSString stringWithFormat:@"%@ = %@", key, ASGetDescriptionValueString(obj)];
}
[components addObject:str];
}];
}
return [components componentsJoinedByString:@"; "];
}
NSString *ASObjectDescriptionMakeWithoutObject(NSArray<NSDictionary *> * _Nullable propertyGroups)
{
return [NSString stringWithFormat:@"{ %@ }", _ASObjectDescriptionMakePropertyList(propertyGroups)];
}
NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray<NSDictionary *> *propertyGroups)
{
if (object == nil) {
return @"(null)";
}
NSMutableString *str = [NSMutableString stringWithFormat:@"<%s: %p", object_getClassName(object), object];
NSString *propList = _ASObjectDescriptionMakePropertyList(propertyGroups);
if (propList.length > 0) {
[str appendFormat:@"; %@", propList];
}
[str appendString:@">"];
return str;
}
NSString *ASObjectDescriptionMakeTiny(__autoreleasing id object) {
return ASObjectDescriptionMake(object, nil);
}
NSString *ASStringWithQuotesIfMultiword(NSString *string) {
if (string == nil) {
return nil;
}
if ([string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location != NSNotFound) {
return [NSString stringWithFormat:@"\"%@\"", string];
} else {
return string;
}
}

View File

@ -0,0 +1,50 @@
//
// ASPendingStateController.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
@class ASDisplayNode;
NS_ASSUME_NONNULL_BEGIN
/**
A singleton that is responsible for applying changes to
UIView/CALayer properties of display nodes when they
have been set on background threads.
This controller will enqueue run-loop events to flush changes
but if you need them flushed now you can call `flush` from the main thread.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASPendingStateController : NSObject
+ (ASPendingStateController *)sharedInstance;
@property (nonatomic, readonly) BOOL hasChanges;
/**
Flush all pending states for nodes now. Any UIView/CALayer properties
that have been set in the background will be applied to their
corresponding views/layers before this method returns.
You must call this method on the main thread.
*/
- (void)flush;
/**
Register this node as having pending state that needs to be copied
over to the view/layer. This is called automatically by display nodes
when their view/layer properties are set post-load on background threads.
*/
- (void)registerNode:(ASDisplayNode *)node;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,102 @@
//
// ASPendingStateController.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASPendingStateController.h"
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASWeakSet.h>
#import "ASDisplayNodeInternal.h" // Required for -applyPendingViewState; consider moving this to +FrameworkPrivate
@interface ASPendingStateController()
{
AS::Mutex _lock;
struct ASPendingStateControllerFlags {
unsigned pendingFlush:1;
} _flags;
}
@property (nonatomic, readonly) ASWeakSet<ASDisplayNode *> *dirtyNodes;
@end
@implementation ASPendingStateController
#pragma mark Lifecycle & Singleton
- (instancetype)init
{
self = [super init];
if (self) {
_dirtyNodes = [[ASWeakSet alloc] init];
}
return self;
}
+ (ASPendingStateController *)sharedInstance
{
static dispatch_once_t onceToken;
static ASPendingStateController *controller = nil;
dispatch_once(&onceToken, ^{
controller = [[ASPendingStateController alloc] init];
});
return controller;
}
#pragma mark External API
- (void)registerNode:(ASDisplayNode *)node
{
ASDisplayNodeAssert(node.nodeLoaded, @"Expected display node to be loaded before it was registered with ASPendingStateController. Node: %@", node);
AS::MutexLocker l(_lock);
[_dirtyNodes addObject:node];
[self scheduleFlushIfNeeded];
}
- (void)flush
{
ASDisplayNodeAssertMainThread();
_lock.lock();
ASWeakSet *dirtyNodes = _dirtyNodes;
_dirtyNodes = [[ASWeakSet alloc] init];
_flags.pendingFlush = NO;
_lock.unlock();
for (ASDisplayNode *node in dirtyNodes) {
[node applyPendingViewState];
}
}
#pragma mark Private Methods
/**
This method is assumed to be called with the lock held.
*/
- (void)scheduleFlushIfNeeded
{
if (_flags.pendingFlush) {
return;
}
_flags.pendingFlush = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self flush];
});
}
@end
@implementation ASPendingStateController (Testing)
- (BOOL)test_isFlushScheduled
{
return _flags.pendingFlush;
}
@end

View File

@ -0,0 +1,83 @@
//
// ASRecursiveUnfairLock.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASRecursiveUnfairLock.h>
#import <stdatomic.h>
/**
* For our atomic _thread, we use acquire/release memory order so that we can have
* the minimum possible constraint on the hardware. The default, `memory_order_seq_cst`
* demands that there be a total order of all such modifications as seen by all threads.
* Acquire/release only requires that modifications to this specific atomic are
* synchronized across acquire/release pairs.
* http://en.cppreference.com/w/cpp/atomic/memory_order
*
* Note also that the unfair_lock involves a thread fence as well, so we don't need to
* take care of synchronizing other values. Just the thread value.
*/
#define rul_set_thread(l, t) atomic_store_explicit(&l->_thread, t, memory_order_release)
#define rul_get_thread(l) atomic_load_explicit(&l->_thread, memory_order_acquire)
void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l)
{
// Try to lock without blocking. If we fail, check what thread owns it.
// Note that the owning thread CAN CHANGE freely, but it can't become `self`
// because only we are `self`. And if it's already `self` then we already have
// the lock, because we reset it to NULL before we unlock. So (thread == self) is
// invariant.
const pthread_t s = pthread_self();
if (os_unfair_lock_trylock(&l->_lock)) {
// Owned by nobody. We now have the lock. Assign self.
rul_set_thread(l, s);
} else if (rul_get_thread(l) == s) {
// Owned by self (recursive lock). nop.
} else {
// Owned by other thread. Block and then set thread to self.
os_unfair_lock_lock(&l->_lock);
rul_set_thread(l, s);
}
l->_count++;
}
BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l)
{
// Same as Lock above. See comments there.
const pthread_t s = pthread_self();
if (os_unfair_lock_trylock(&l->_lock)) {
// Owned by nobody. We now have the lock. Assign self.
rul_set_thread(l, s);
} else if (rul_get_thread(l) == s) {
// Owned by self (recursive lock). nop.
} else {
// Owned by other thread. Fail.
return NO;
}
l->_count++;
return YES;
}
void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l)
{
// Ensure we have the lock. This check may miss some pathological cases,
// but it'll catch 99.999999% of this serious programmer error.
NSCAssert(rul_get_thread(l) == pthread_self(), @"Unlocking from a different thread than locked.");
if (0 == --l->_count) {
// Note that we have to clear this before unlocking because, if another thread
// succeeds in locking above, but hasn't managed to update _thread, and we
// try to re-lock, and fail the -tryLock, and read _thread, then we'll mistakenly
// think that we still own the lock and proceed without blocking.
rul_set_thread(l, NULL);
os_unfair_lock_unlock(&l->_lock);
}
}

View File

@ -0,0 +1,29 @@
//
// ASResponderChainEnumerator.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIResponder.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
NS_ASSUME_NONNULL_BEGIN
AS_SUBCLASSING_RESTRICTED
@interface ASResponderChainEnumerator : NSEnumerator
- (instancetype)initWithResponder:(UIResponder *)responder;
@end
@interface UIResponder (ASResponderChainEnumerator)
- (ASResponderChainEnumerator *)asdk_responderChainEnumerator;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,45 @@
//
// ASResponderChainEnumerator.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASResponderChainEnumerator.h"
#import <AsyncDisplayKit/ASAssert.h>
@implementation ASResponderChainEnumerator {
UIResponder *_currentResponder;
}
- (instancetype)initWithResponder:(UIResponder *)responder
{
ASDisplayNodeAssertMainThread();
if (self = [super init]) {
_currentResponder = responder;
}
return self;
}
#pragma mark - NSEnumerator
- (id)nextObject
{
ASDisplayNodeAssertMainThread();
id result = [_currentResponder nextResponder];
_currentResponder = result;
return result;
}
@end
@implementation UIResponder (ASResponderChainEnumerator)
- (NSEnumerator *)asdk_responderChainEnumerator
{
return [[ASResponderChainEnumerator alloc] initWithResponder:self];
}
@end

View File

@ -0,0 +1,464 @@
//
// ASRunLoopQueue.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASThread.h>
#import "ASSignpost.h"
#import <QuartzCore/QuartzCore.h>
#import <cstdlib>
#import <deque>
#import <vector>
#define ASRunLoopQueueLoggingEnabled 0
#define ASRunLoopQueueVerboseLoggingEnabled 0
using AS::MutexLocker;
static void runLoopSourceCallback(void *info) {
// No-op
#if ASRunLoopQueueVerboseLoggingEnabled
NSLog(@"<%@> - Called runLoopSourceCallback", info);
#endif
}
#pragma mark - ASDeallocQueue
@implementation ASDeallocQueue {
std::vector<CFTypeRef> _queue;
AS::Mutex _lock;
}
+ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED
{
static ASDeallocQueue *deallocQueue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
deallocQueue = [[ASDeallocQueue alloc] init];
});
return deallocQueue;
}
- (void)dealloc
{
ASDisplayNodeFailAssert(@"Singleton should not dealloc.");
}
- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr
{
NSParameterAssert(objectPtr != NULL);
// Cast to CFType so we can manipulate retain count manually.
const auto cfPtr = (CFTypeRef *)(void *)objectPtr;
if (!cfPtr || !*cfPtr) {
return;
}
_lock.lock();
const auto isFirstEntry = _queue.empty();
// Push the pointer into our queue and clear their pointer.
// This "steals" the +1 from ARC and nils their pointer so they can't
// access or release the object.
_queue.push_back(*cfPtr);
*cfPtr = NULL;
_lock.unlock();
if (isFirstEntry) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.100 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[self drain];
});
}
}
- (void)drain
{
_lock.lock();
const auto q = std::move(_queue);
_lock.unlock();
for (CFTypeRef ref : q) {
// NOTE: Could check that retain count is 1 and retry later if not.
CFRelease(ref);
}
}
@end
@implementation ASAbstractRunLoopQueue
- (instancetype)init
{
self = [super init];
if (self == nil) {
return nil;
}
ASDisplayNodeAssert(self.class != [ASAbstractRunLoopQueue class], @"Should never create instances of abstract class ASAbstractRunLoopQueue.");
return self;
}
@end
#pragma mark - ASRunLoopQueue
@interface ASRunLoopQueue () {
CFRunLoopRef _runLoop;
CFRunLoopSourceRef _runLoopSource;
CFRunLoopObserverRef _runLoopObserver;
NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance.
AS::RecursiveMutex _internalQueueLock;
#if ASRunLoopQueueLoggingEnabled
NSTimer *_runloopQueueLoggingTimer;
#endif
}
@property (nonatomic) void (^queueConsumer)(id dequeuedItem, BOOL isQueueDrained);
@end
@implementation ASRunLoopQueue
- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock
{
if (self = [super init]) {
_runLoop = runloop;
NSPointerFunctionsOptions options = retainsObjects ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory;
_internalQueue = [[NSPointerArray alloc] initWithOptions:options];
_queueConsumer = handlerBlock;
_batchSize = 1;
_ensureExclusiveMembership = YES;
// Self is guaranteed to outlive the observer. Without the high cost of a weak pointer,
// __unsafe_unretained allows us to avoid flagging the memory cycle detector.
__unsafe_unretained __typeof__(self) weakSelf = self;
void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[weakSelf processQueue];
};
_runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock);
CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes);
// It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of
// the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done
CFRunLoopSourceContext sourceContext = {};
sourceContext.perform = runLoopSourceCallback;
#if ASRunLoopQueueLoggingEnabled
sourceContext.info = (__bridge void *)self;
#endif
_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes);
#if ASRunLoopQueueLoggingEnabled
_runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes];
#endif
}
return self;
}
- (void)dealloc
{
if (CFRunLoopContainsSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes)) {
CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes);
}
CFRelease(_runLoopSource);
_runLoopSource = nil;
if (CFRunLoopObserverIsValid(_runLoopObserver)) {
CFRunLoopObserverInvalidate(_runLoopObserver);
}
CFRelease(_runLoopObserver);
_runLoopObserver = nil;
}
#if ASRunLoopQueueLoggingEnabled
- (void)checkRunLoop
{
NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count);
}
#endif
- (void)processQueue
{
BOOL hasExecutionBlock = (_queueConsumer != nil);
// If we have an execution block, this vector will be populated, otherwise remains empty.
// This is to avoid needlessly retaining/releasing the objects if we don't have a block.
std::vector<id> itemsToProcess;
BOOL isQueueDrained = NO;
{
MutexLocker l(_internalQueueLock);
NSInteger internalQueueCount = _internalQueue.count;
// Early-exit if the queue is empty.
if (internalQueueCount == 0) {
return;
}
ASSignpostStart(ASSignpostRunLoopQueueBatch);
// Snatch the next batch of items.
NSInteger maxCountToProcess = MIN(internalQueueCount, self.batchSize);
/**
* For each item in the next batch, if it's non-nil then NULL it out
* and if we have an execution block then add it in.
* This could be written a bunch of different ways but
* this particular one nicely balances readability, safety, and efficiency.
*/
NSInteger foundItemCount = 0;
for (NSInteger i = 0; i < internalQueueCount && foundItemCount < maxCountToProcess; i++) {
/**
* It is safe to use unsafe_unretained here. If the queue is weak, the
* object will be added to the autorelease pool. If the queue is strong,
* it will retain the object until we transfer it (retain it) in itemsToProcess.
*/
__unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i];
if (ptr != nil) {
foundItemCount++;
if (hasExecutionBlock) {
itemsToProcess.push_back(ptr);
}
[_internalQueue replacePointerAtIndex:i withPointer:NULL];
}
}
if (foundItemCount == 0) {
// If _internalQueue holds weak references, and all of them just become NULL, then the array
// is never marked as needsCompletion, and compact will return early, not removing the NULL's.
// Inserting a NULL here ensures the compaction will take place.
// See http://www.openradar.me/15396578 and https://stackoverflow.com/a/40274426/1136669
[_internalQueue addPointer:NULL];
}
[_internalQueue compact];
if (_internalQueue.count == 0) {
isQueueDrained = YES;
}
}
// itemsToProcess will be empty if _queueConsumer == nil so no need to check again.
const auto count = itemsToProcess.size();
if (count > 0) {
const auto itemsEnd = itemsToProcess.cend();
for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) {
__unsafe_unretained id value = *iterator;
_queueConsumer(value, isQueueDrained && iterator == itemsEnd - 1);
}
}
// If the queue is not fully drained yet force another run loop to process next batch of items
if (!isQueueDrained) {
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(_runLoop);
}
ASSignpostEnd(ASSignpostRunLoopQueueBatch);
}
- (void)enqueue:(id)object
{
if (!object) {
return;
}
MutexLocker l(_internalQueueLock);
// Check if the object exists.
BOOL foundObject = NO;
if (_ensureExclusiveMembership) {
for (id currentObject in _internalQueue) {
if (currentObject == object) {
foundObject = YES;
break;
}
}
}
if (!foundObject) {
[_internalQueue addPointer:(__bridge void *)object];
if (_internalQueue.count == 1) {
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(_runLoop);
}
}
}
- (BOOL)isEmpty
{
MutexLocker l(_internalQueueLock);
return _internalQueue.count == 0;
}
ASSynthesizeLockingMethodsWithMutex(_internalQueueLock)
@end
#pragma mark - ASCATransactionQueue
@interface ASCATransactionQueue () {
CFRunLoopSourceRef _runLoopSource;
CFRunLoopObserverRef _preTransactionObserver;
// Current buffer for new entries, only accessed from within its mutex.
std::vector<id<ASCATransactionQueueObserving>> _internalQueue;
// No retain, no release, pointer hash, pointer equality.
// Enforce uniqueness in our queue. std::unordered_set does a heap allocation for each entry not good.
CFMutableSetRef _internalQueueHashSet;
// Temporary buffer, only accessed from the main thread in -process.
std::vector<id<ASCATransactionQueueObserving>> _batchBuffer;
AS::Mutex _internalQueueLock;
// In order to not pollute the top-level activities, each queue has 1 root activity.
#if ASRunLoopQueueLoggingEnabled
NSTimer *_runloopQueueLoggingTimer;
#endif
}
@end
@implementation ASCATransactionQueue
// CoreAnimation commit order is 2000000, the goal of this is to process shortly beforehand
// but after most other scheduled work on the runloop has processed.
static int const kASASCATransactionQueueOrder = 1000000;
ASCATransactionQueue *_ASSharedCATransactionQueue;
dispatch_once_t _ASSharedCATransactionQueueOnceToken;
- (instancetype)init
{
if (self = [super init]) {
_internalQueueHashSet = CFSetCreateMutable(NULL, 0, NULL);
// This is going to be a very busy queue every node in the preload range will enter this queue.
// Save some time on first render by reserving space up front.
static constexpr int kInternalQueueInitialCapacity = 64;
_internalQueue.reserve(kInternalQueueInitialCapacity);
_batchBuffer.reserve(kInternalQueueInitialCapacity);
// Self is guaranteed to outlive the observer. Without the high cost of a weak pointer,
// __unsafe_unretained allows us to avoid flagging the memory cycle detector.
__unsafe_unretained __typeof__(self) weakSelf = self;
_preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
while (!weakSelf->_internalQueue.empty()) {
[weakSelf processQueue];
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), _preTransactionObserver, kCFRunLoopCommonModes);
// It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of
// the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done
CFRunLoopSourceContext sourceContext = {};
sourceContext.perform = runLoopSourceCallback;
#if ASRunLoopQueueLoggingEnabled
sourceContext.info = (__bridge void *)self;
#endif
_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
CFRunLoopAddSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes);
#if ASRunLoopQueueLoggingEnabled
_runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes];
#endif
}
return self;
}
- (void)dealloc
{
ASDisplayNodeAssertMainThread();
CFRelease(_internalQueueHashSet);
CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes);
CFRelease(_runLoopSource);
_runLoopSource = nil;
if (CFRunLoopObserverIsValid(_preTransactionObserver)) {
CFRunLoopObserverInvalidate(_preTransactionObserver);
}
CFRelease(_preTransactionObserver);
_preTransactionObserver = nil;
}
#if ASRunLoopQueueLoggingEnabled
- (void)checkRunLoop
{
NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count);
}
#endif
- (void)processQueue
{
ASDisplayNodeAssertMainThread();
AS::UniqueLock l(_internalQueueLock);
NSInteger count = _internalQueue.size();
// Early-exit if the queue is empty.
if (count == 0) {
return;
}
ASSignpostStart(ASSignpostRunLoopQueueBatch);
// Swap buffers, clear our hash table.
_internalQueue.swap(_batchBuffer);
CFSetRemoveAllValues(_internalQueueHashSet);
// Unlock early. We are done with internal queue, and batch buffer is main-thread-only so no lock.
l.unlock();
for (const id<ASCATransactionQueueObserving> &value : _batchBuffer) {
[value prepareForCATransactionCommit];
}
_batchBuffer.clear();
ASSignpostEnd(ASSignpostRunLoopQueueBatch);
}
- (void)enqueue:(id<ASCATransactionQueueObserving>)object
{
if (!object) {
return;
}
if (!self.enabled) {
[object prepareForCATransactionCommit];
return;
}
MutexLocker l(_internalQueueLock);
if (CFSetContainsValue(_internalQueueHashSet, (__bridge void *)object)) {
return;
}
CFSetAddValue(_internalQueueHashSet, (__bridge void *)object);
_internalQueue.emplace_back(object);
if (_internalQueue.size() == 1) {
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(CFRunLoopGetMain());
}
}
- (BOOL)isEmpty
{
MutexLocker l(_internalQueueLock);
return _internalQueue.empty();
}
- (BOOL)isEnabled
{
return ASActivateExperimentalFeature(ASExperimentalInterfaceStateCoalescing);
}
@end

View File

@ -0,0 +1,64 @@
//
// ASScrollDirection.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASScrollDirection.h>
const ASScrollDirection ASScrollDirectionHorizontalDirections = ASScrollDirectionLeft | ASScrollDirectionRight;
const ASScrollDirection ASScrollDirectionVerticalDirections = ASScrollDirectionUp | ASScrollDirectionDown;
BOOL ASScrollDirectionContainsVerticalDirection(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionVerticalDirections) != 0;
}
BOOL ASScrollDirectionContainsHorizontalDirection(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionHorizontalDirections) != 0;
}
BOOL ASScrollDirectionContainsRight(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionRight) != 0;
}
BOOL ASScrollDirectionContainsLeft(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionLeft) != 0;
}
BOOL ASScrollDirectionContainsUp(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionUp) != 0;
}
BOOL ASScrollDirectionContainsDown(ASScrollDirection scrollDirection) {
return (scrollDirection & ASScrollDirectionDown) != 0;
}
ASScrollDirection ASScrollDirectionInvertHorizontally(ASScrollDirection scrollDirection) {
if (scrollDirection == ASScrollDirectionRight) {
return ASScrollDirectionLeft;
} else if (scrollDirection == ASScrollDirectionLeft) {
return ASScrollDirectionRight;
}
return scrollDirection;
}
ASScrollDirection ASScrollDirectionInvertVertically(ASScrollDirection scrollDirection) {
if (scrollDirection == ASScrollDirectionUp) {
return ASScrollDirectionDown;
} else if (scrollDirection == ASScrollDirectionDown) {
return ASScrollDirectionUp;
}
return scrollDirection;
}
ASScrollDirection ASScrollDirectionApplyTransform(ASScrollDirection scrollDirection, CGAffineTransform transform) {
if ((transform.a < 0) && ASScrollDirectionContainsHorizontalDirection(scrollDirection)) {
return ASScrollDirectionInvertHorizontally(scrollDirection);
} else if ((transform.d < 0) && ASScrollDirectionContainsVerticalDirection(scrollDirection)) {
return ASScrollDirectionInvertVertically(scrollDirection);
}
return scrollDirection;
}

View File

@ -0,0 +1,178 @@
//
// ASScrollNode.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASScrollNode.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASThread.h>
@interface ASScrollView : UIScrollView
@end
@implementation ASScrollView
// This special +layerClass allows ASScrollNode to get -layout calls from -layoutSublayers.
+ (Class)layerClass
{
return [_ASDisplayLayer class];
}
- (ASScrollNode *)scrollNode
{
return (ASScrollNode *)ASViewToDisplayNode(self);
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
if ([[self scrollNode] canCancelAllTouchesInViews]) {
return true;
}
return [super touchesShouldCancelInContentView:view];
}
#pragma mark - _ASDisplayView behavior substitutions
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
- (void)willMoveToWindow:(UIWindow *)newWindow
{
ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar.
BOOL visible = (newWindow != nil);
if (visible && !node.inHierarchy) {
[node __enterHierarchy];
}
}
- (void)didMoveToWindow
{
ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar.
BOOL visible = (self.window != nil);
if (!visible && node.inHierarchy) {
[node __exitHierarchy];
}
}
- (NSArray *)accessibilityElements
{
return [self.asyncdisplaykit_node accessibilityElements];
}
@end
@implementation ASScrollNode
{
ASScrollDirection _scrollableDirections;
BOOL _automaticallyManagesContentSize;
CGSize _contentCalculatedSizeFromLayout;
}
@dynamic view;
- (instancetype)init
{
if (self = [super init]) {
[self setViewBlock:^UIView *{ return [[ASScrollView alloc] init]; }];
}
return self;
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
restrictedToSize:(ASLayoutElementSize)size
relativeToParentSize:(CGSize)parentSize
{
ASScopedLockSelfOrToRoot();
ASSizeRange contentConstrainedSize = constrainedSize;
if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) {
contentConstrainedSize.max.height = CGFLOAT_MAX;
}
if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections)) {
contentConstrainedSize.max.width = CGFLOAT_MAX;
}
ASLayout *layout = [super calculateLayoutThatFits:contentConstrainedSize
restrictedToSize:size
relativeToParentSize:parentSize];
if (_automaticallyManagesContentSize) {
// To understand this code, imagine we're containing a horizontal stack set within a vertical table node.
// Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height.
// In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt.
// We can achieve this behavior by:
// 1. Always set contentSize to layout.size.
// 2. Set bounds to a size that is calculated by clamping parentSize against constrained size,
// unless one dimension is not defined, in which case adopt the contentSize for that dimension.
_contentCalculatedSizeFromLayout = layout.size;
CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize);
if (ASPointsValidForLayout(selfSize.width) == NO) {
selfSize.width = _contentCalculatedSizeFromLayout.width;
}
if (ASPointsValidForLayout(selfSize.height) == NO) {
selfSize.height = _contentCalculatedSizeFromLayout.height;
}
// Don't provide a position, as that should be set by the parent.
layout = [ASLayout layoutWithLayoutElement:self
size:selfSize
sublayouts:layout.sublayouts];
}
return layout;
}
- (void)layout
{
[super layout];
ASLockScopeSelf(); // Lock for using our two instance variables.
if (_automaticallyManagesContentSize) {
CGSize contentSize = _contentCalculatedSizeFromLayout;
if (ASIsCGSizeValidForLayout(contentSize) == NO) {
NSLog(@"%@ calculated a size in its layout spec that can't be applied to .contentSize: %@. Applying parentSize (scrollNode's bounds) instead: %@.", self, NSStringFromCGSize(contentSize), NSStringFromCGSize(self.calculatedSize));
contentSize = self.calculatedSize;
}
self.view.contentSize = contentSize;
}
}
- (BOOL)automaticallyManagesContentSize
{
ASLockScopeSelf();
return _automaticallyManagesContentSize;
}
- (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize
{
ASLockScopeSelf();
_automaticallyManagesContentSize = automaticallyManagesContentSize;
if (_automaticallyManagesContentSize == YES
&& ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO
&& ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) {
// Set the @default value, for more user-friendly behavior of the most
// common use cases of .automaticallyManagesContentSize.
_scrollableDirections = ASScrollDirectionVerticalDirections;
}
}
- (ASScrollDirection)scrollableDirections
{
ASLockScopeSelf();
return _scrollableDirections;
}
- (void)setScrollableDirections:(ASScrollDirection)scrollableDirections
{
ASLockScopeSelf();
if (_scrollableDirections != scrollableDirections) {
_scrollableDirections = scrollableDirections;
[self setNeedsLayout];
}
}
@end

View File

@ -0,0 +1,94 @@
//
// ASSignpost.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
/// The signposts we use. Signposts are grouped by color. The SystemTrace.tracetemplate file
/// should be kept up-to-date with these values.
typedef NS_ENUM(uint32_t, ASSignpostName) {
// Collection/Table (Blue)
ASSignpostDataControllerBatch = 300, // Alloc/layout nodes before collection update.
ASSignpostRangeControllerUpdate, // Ranges update pass.
ASSignpostCollectionUpdate, // Entire update process, from -endUpdates to [super perform…]
// Rendering (Green)
ASSignpostLayerDisplay = 325, // Client display callout.
ASSignpostRunLoopQueueBatch, // One batch of ASRunLoopQueue.
// Layout (Purple)
ASSignpostCalculateLayout = 350, // Start of calculateLayoutThatFits to end. Max 1 per thread.
// Misc (Orange)
ASSignpostDeallocQueueDrain = 375, // One chunk of dealloc queue work. arg0 is count.
ASSignpostCATransactionLayout, // The CA transaction commit layout phase.
ASSignpostCATransactionCommit // The CA transaction commit post-layout phase.
};
typedef NS_ENUM(uintptr_t, ASSignpostColor) {
ASSignpostColorBlue,
ASSignpostColorGreen,
ASSignpostColorPurple,
ASSignpostColorOrange,
ASSignpostColorRed,
ASSignpostColorDefault
};
static inline ASSignpostColor ASSignpostGetColor(ASSignpostName name, ASSignpostColor colorPref) {
if (colorPref == ASSignpostColorDefault) {
return (ASSignpostColor)((name / 25) % 4);
} else {
return colorPref;
}
}
#if defined(PROFILE) && __has_include(<sys/kdebug_signpost.h>)
#define AS_KDEBUG_ENABLE 1
#else
#define AS_KDEBUG_ENABLE 0
#endif
#if AS_KDEBUG_ENABLE
#import <sys/kdebug_signpost.h>
// These definitions are required to build the backward-compatible kdebug trace
// on the iOS 10 SDK. The kdebug_trace function crashes if run on iOS 9 and earlier.
// It's valuable to support trace signposts on iOS 9, because A5 devices don't support iOS 10.
#ifndef DBG_MACH_CHUD
#define DBG_MACH_CHUD 0x0A
#define DBG_FUNC_NONE 0
#define DBG_FUNC_START 1
#define DBG_FUNC_END 2
#define DBG_APPS 33
#define SYS_kdebug_trace 180
#define KDBG_CODE(Class, SubClass, code) (((Class & 0xff) << 24) | ((SubClass & 0xff) << 16) | ((code & 0x3fff) << 2))
#define APPSDBG_CODE(SubClass,code) KDBG_CODE(DBG_APPS, SubClass, code)
#endif
// Currently we'll reserve arg3.
#define ASSignpost(name, identifier, arg2, color) \
AS_AT_LEAST_IOS10 ? kdebug_signpost(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \
: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_NONE, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color));
#define ASSignpostStartCustom(name, identifier, arg2) \
AS_AT_LEAST_IOS10 ? kdebug_signpost_start(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0) \
: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_START, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0);
#define ASSignpostStart(name) ASSignpostStartCustom(name, self, 0)
#define ASSignpostEndCustom(name, identifier, arg2, color) \
AS_AT_LEAST_IOS10 ? kdebug_signpost_end(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \
: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_END, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color));
#define ASSignpostEnd(name) ASSignpostEndCustom(name, self, 0, ASSignpostColorDefault)
#else
#define ASSignpost(name, identifier, arg2, color)
#define ASSignpostStartCustom(name, identifier, arg2)
#define ASSignpostStart(name)
#define ASSignpostEndCustom(name, identifier, arg2, color)
#define ASSignpostEnd(name)
#endif

View File

@ -0,0 +1,194 @@
//
// ASTextKitComponents.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASTextKitComponents.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
#import <tgmath.h>
@interface ASTextKitComponentsTextView () {
// Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860
BOOL _deallocating;
}
@property CGRect threadSafeBounds;
@end
@implementation ASTextKitComponentsTextView
- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer
{
self = [super initWithFrame:frame textContainer:textContainer];
if (self) {
_threadSafeBounds = self.bounds;
_deallocating = NO;
}
return self;
}
- (void)dealloc
{
_deallocating = YES;
}
- (void)setFrame:(CGRect)frame
{
ASDisplayNodeAssertMainThread();
[super setFrame:frame];
self.threadSafeBounds = self.bounds;
}
- (void)setBounds:(CGRect)bounds
{
ASDisplayNodeAssertMainThread();
[super setBounds:bounds];
self.threadSafeBounds = bounds;
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if (_deallocating) {
return;
}
[super setContentOffset:contentOffset];
}
@end
@interface ASTextKitComponents ()
// read-write redeclarations
@property (nonatomic) NSTextStorage *textStorage;
@property (nonatomic) NSTextContainer *textContainer;
@property (nonatomic) NSLayoutManager *layoutManager;
@end
@implementation ASTextKitComponents
#pragma mark - Class
+ (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString
textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED
{
NSTextStorage *textStorage = attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init];
return [self componentsWithTextStorage:textStorage
textContainerSize:textContainerSize
layoutManager:[[NSLayoutManager alloc] init]];
}
+ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage
textContainerSize:(CGSize)textContainerSize
layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED
{
ASTextKitComponents *components = [[self alloc] init];
components.textStorage = textStorage;
components.layoutManager = layoutManager;
[components.textStorage addLayoutManager:components.layoutManager];
components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize];
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
[components.layoutManager addTextContainer:components.textContainer];
return components;
}
+ (BOOL)needsMainThreadDeallocation
{
return YES;
}
#pragma mark - Lifecycle
- (void)dealloc
{
// Nil out all delegates to prevent crash
if (_textView) {
ASDisplayNodeAssertMainThread();
_textView.delegate = nil;
}
_layoutManager.delegate = nil;
}
#pragma mark - Sizing
- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth
{
ASTextKitComponents *components = self;
// If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation.
// Otherwise, we create a temporary stack to size for `constrainedWidth`.
if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) {
components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)];
}
// Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:).
[components.layoutManager ensureLayoutForTextContainer:components.textContainer];
CGSize textSize = [components.layoutManager usedRectForTextContainer:components.textContainer].size;
return textSize;
}
- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth
forMaxNumberOfLines:(NSInteger)maxNumberOfLines
{
if (maxNumberOfLines == 0) {
return [self sizeForConstrainedWidth:constrainedWidth];
}
ASTextKitComponents *components = self;
// Always use temporary stack in case of threading issues
components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)];
// Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:).
[components.layoutManager ensureLayoutForTextContainer:components.textContainer];
CGFloat width = [components.layoutManager usedRectForTextContainer:components.textContainer].size.width;
// Calculate height based on line fragments
// Based on calculating number of lines from: http://asciiwwdc.com/2013/sessions/220
NSRange glyphRange, lineRange = NSMakeRange(0, 0);
CGRect rect = CGRectZero;
CGFloat height = 0;
CGFloat lastOriginY = -1.0;
NSUInteger numberOfLines = 0;
glyphRange = [components.layoutManager glyphRangeForTextContainer:components.textContainer];
while (lineRange.location < NSMaxRange(glyphRange)) {
rect = [components.layoutManager lineFragmentRectForGlyphAtIndex:lineRange.location
effectiveRange:&lineRange];
if (CGRectGetMinY(rect) > lastOriginY) {
++numberOfLines;
if (numberOfLines == maxNumberOfLines) {
height = rect.origin.y + rect.size.height;
break;
}
}
lastOriginY = CGRectGetMinY(rect);
lineRange.location = NSMaxRange(lineRange);
}
CGFloat fragmentHeight = rect.origin.y + rect.size.height;
CGFloat finalHeight = std::ceil(std::fmax(height, fragmentHeight));
CGSize size = CGSizeMake(width, finalHeight);
return size;
}
@end

View File

@ -0,0 +1,53 @@
//
// ASTextKitContext.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASAvailability.h>
#if AS_ENABLE_TEXTNODE
#import <AsyncDisplayKit/ASBaseDefines.h>
/**
A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text.
This container is the sole owner and manager of the TextKit classes. This is an important model because of major
thread safety issues inside vanilla TextKit. It provides a central locking location for accessing TextKit methods.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASTextKitContext : NSObject
/**
Initializes a context and its associated TextKit components.
Initialization of TextKit components is a globally locking operation so be careful of bottlenecks with this class.
*/
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
lineBreakMode:(NSLineBreakMode)lineBreakMode
maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
exclusionPaths:(NSArray *)exclusionPaths
constrainedSize:(CGSize)constrainedSize;
/**
All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to
TextKit components may cause crashes.
The block provided MUST not call out to client code from within its scope or it is possible for this to cause deadlocks
in your application. Use with EXTREME care.
Callers MUST NOT keep a ref to these internal objects and use them later. This WILL cause crashes in your application.
*/
- (void)performBlockWithLockedTextKitComponents:(AS_NOESCAPE void (^)(NSLayoutManager *layoutManager,
NSTextStorage *textStorage,
NSTextContainer *textContainer))block;
@end
#endif

View File

@ -0,0 +1,84 @@
//
// ASTextKitContext.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASTextKitContext.h"
#if AS_ENABLE_TEXTNODE
#import "ASLayoutManager.h"
#import <AsyncDisplayKit/ASThread.h>
#include <memory>
@implementation ASTextKitContext
{
// All TextKit operations (even non-mutative ones) must be executed serially.
std::shared_ptr<AS::Mutex> __instanceLock__;
NSLayoutManager *_layoutManager;
NSTextStorage *_textStorage;
NSTextContainer *_textContainer;
}
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
lineBreakMode:(NSLineBreakMode)lineBreakMode
maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
exclusionPaths:(NSArray *)exclusionPaths
constrainedSize:(CGSize)constrainedSize
{
if (self = [super init]) {
static dispatch_once_t onceToken;
static AS::Mutex *mutex;
dispatch_once(&onceToken, ^{
mutex = new AS::Mutex();
});
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock.
AS::MutexLocker l(*mutex);
__instanceLock__ = std::make_shared<AS::Mutex>();
// Create the TextKit component stack with our default configuration.
_textStorage = [[NSTextStorage alloc] init];
_layoutManager = [[ASLayoutManager alloc] init];
_layoutManager.usesFontLeading = NO;
[_textStorage addLayoutManager:_layoutManager];
// Instead of calling [NSTextStorage initWithAttributedString:], setting attributedString just after calling addlayoutManager can fix CJK language layout issues.
// See https://github.com/facebook/AsyncDisplayKit/issues/2894
if (attributedString) {
[_textStorage setAttributedString:attributedString];
}
_textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
// We want the text laid out up to the very edges of the container.
_textContainer.lineFragmentPadding = 0;
_textContainer.lineBreakMode = lineBreakMode;
_textContainer.maximumNumberOfLines = maximumNumberOfLines;
_textContainer.exclusionPaths = exclusionPaths;
[_layoutManager addTextContainer:_textContainer];
}
return self;
}
- (void)performBlockWithLockedTextKitComponents:(NS_NOESCAPE void (^)(NSLayoutManager *,
NSTextStorage *,
NSTextContainer *))block
{
AS::MutexLocker l(*__instanceLock__);
if (block) {
block(_layoutManager, _textStorage, _textContainer);
}
}
@end
#endif

View File

@ -0,0 +1,34 @@
//
// ASTextNodeCommon.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASAvailability.h>
#define AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE() { \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
NSLog(@"[Texture] Warning: Feature %@ is unimplemented in %@.", NSStringFromSelector(_cmd), NSStringFromClass(self.class)); \
});\
}
/**
* Highlight styles.
*/
typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) {
/**
* Highlight style for text on a light background.
*/
ASTextNodeHighlightStyleLight,
/**
* Highlight style for text on a dark background.
*/
ASTextNodeHighlightStyleDark
};

View File

@ -0,0 +1,37 @@
//
// ASTextNodeWordKerner.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/NSLayoutManager.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
NS_ASSUME_NONNULL_BEGIN
/**
@abstract This class acts as the NSLayoutManagerDelegate for ASTextNode.
@discussion Its current job is word kerning, i.e. adjusting the width of spaces to match the set
wordKernedSpaceWidth. If word kerning is not needed, set the layoutManager's delegate to nil.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASTextNodeWordKerner : NSObject <NSLayoutManagerDelegate>
/**
The following @optional NSLayoutManagerDelegate methods are implemented:
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange NS_AVAILABLE_IOS(7_0);
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0);
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0);
*/
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,130 @@
//
// ASTextNodeWordKerner.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASTextNodeWordKerner.h"
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASTextNodeTypes.h>
@implementation ASTextNodeWordKerner
#pragma mark - NSLayoutManager Delegate
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange
{
NSUInteger glyphCount = glyphRange.length;
NSGlyphProperty *newGlyphProperties = NULL;
BOOL usesWordKerning = NO;
// If our typing attributes specify word kerning, specify the spaces as whitespace control characters so we can customize their width.
// Are any of the characters spaces?
NSString *textStorageString = layoutManager.textStorage.string;
for (NSUInteger arrayIndex = 0; arrayIndex < glyphCount; arrayIndex++) {
NSUInteger characterIndex = characterIndexes[arrayIndex];
if ([textStorageString characterAtIndex:characterIndex] != ' ')
continue;
// If we've set the whitespace control character for this space already, we have nothing to do.
if (properties[arrayIndex] == NSGlyphPropertyControlCharacter) {
usesWordKerning = YES;
continue;
}
// Create new glyph properties, if necessary.
if (!newGlyphProperties) {
newGlyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount);
memcpy(newGlyphProperties, properties, (sizeof(NSGlyphProperty) * glyphCount));
}
// It's a space. Make it a whitespace control character.
newGlyphProperties[arrayIndex] = NSGlyphPropertyControlCharacter;
}
// If we don't have any custom glyph properties, return 0 to indicate to the layout manager that it should use the standard glyphs+properties.
if (!newGlyphProperties) {
if (usesWordKerning) {
// If the text does use word kerning we have to make sure we return the correct glyphCount, or the layout manager will just use the default properties and ignore our kerning.
[layoutManager setGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
return glyphCount;
} else {
return 0;
}
}
// Otherwise, use our custom glyph properties.
[layoutManager setGlyphs:glyphs properties:newGlyphProperties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
free(newGlyphProperties);
return glyphCount;
}
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex
{
// If it's a space character and we have custom word kerning, use the whitespace action control character.
if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ')
return NSControlCharacterActionWhitespace;
return defaultAction;
}
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex
{
CGFloat wordKernedSpaceWidth = [self _wordKernedSpaceWidthForCharacterAtIndex:characterIndex atGlyphPosition:glyphPosition forTextContainer:textContainer layoutManager:layoutManager];
return CGRectMake(glyphPosition.x, glyphPosition.y, wordKernedSpaceWidth, CGRectGetHeight(proposedRect));
}
- (CGFloat)_wordKernedSpaceWidthForCharacterAtIndex:(NSUInteger)characterIndex atGlyphPosition:(CGPoint)glyphPosition forTextContainer:(NSTextContainer *)textContainer layoutManager:(NSLayoutManager *)layoutManager
{
// We use a map table for pointer equality and non-copying keys.
static NSMapTable *spaceSizes;
// NSMapTable is a defined thread unsafe class, so we need to synchronize
// access in a light manner. So we use dispatch_sync on this queue for all
// access to the map table.
static dispatch_queue_t mapQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
spaceSizes = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:1];
mapQueue = dispatch_queue_create("org.AsyncDisplayKit.wordKerningQueue", DISPATCH_QUEUE_SERIAL);
});
CGFloat ordinarySpaceWidth;
UIFont *font = [layoutManager.textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
CGFloat wordKerning = [[layoutManager.textStorage attribute:ASTextNodeWordKerningAttributeName atIndex:characterIndex effectiveRange:NULL] floatValue];
__block NSNumber *ordinarySpaceSizeValue;
dispatch_sync(mapQueue, ^{
ordinarySpaceSizeValue = [spaceSizes objectForKey:font];
});
if (ordinarySpaceSizeValue == nil) {
ordinarySpaceWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width;
dispatch_async(mapQueue, ^{
[spaceSizes setObject:@(ordinarySpaceWidth) forKey:font];
});
} else {
ordinarySpaceWidth = [ordinarySpaceSizeValue floatValue];
}
CGFloat totalKernedWidth = (ordinarySpaceWidth + wordKerning);
// TextKit normally handles whitespace by increasing the advance of the previous glyph, rather than displaying an
// actual glyph for the whitespace itself. However, in order to implement word kerning, we explicitly require a
// discrete glyph whose bounding box we can specify. The problem is that TextKit does not know this glyph is
// invisible. From TextKit's perspective, this whitespace glyph is a glyph that MUST be displayed. Thus when it
// comes to determining linebreaks, the width of this trailing whitespace glyph is considered. This causes
// our text to wrap sooner than it otherwise would, as room is allocated at the end of each line for a glyph that
// isn't actually visible. To implement our desired behavior, we check to see if the current whitespace glyph
// would break to the next line. If it breaks to the next line, then this constitutes trailing whitespace, and
// we specify enough room to fill up the remainder of the line, but nothing more.
if (glyphPosition.x + totalKernedWidth > textContainer.size.width) {
return (textContainer.size.width - glyphPosition.x);
}
return totalKernedWidth;
}
@end

View File

@ -0,0 +1,256 @@
//
// ASTraitCollection.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASHashing.h>
#import <AsyncDisplayKit/ASTraitCollection.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#pragma mark - ASPrimitiveTraitCollection
void ASTraitCollectionPropagateDown(id<ASLayoutElement> element, ASPrimitiveTraitCollection traitCollection) {
if (element) {
element.primitiveTraitCollection = traitCollection;
}
for (id<ASLayoutElement> subelement in element.sublayoutElements) {
ASTraitCollectionPropagateDown(subelement, traitCollection);
}
}
ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault() {
ASPrimitiveTraitCollection tc = {};
tc.userInterfaceIdiom = UIUserInterfaceIdiomUnspecified;
tc.forceTouchCapability = UIForceTouchCapabilityUnknown;
tc.displayScale = 0.0;
tc.horizontalSizeClass = UIUserInterfaceSizeClassUnspecified;
tc.verticalSizeClass = UIUserInterfaceSizeClassUnspecified;
tc.containerSize = CGSizeZero;
if (AS_AVAILABLE_IOS(10)) {
tc.displayGamut = UIDisplayGamutUnspecified;
tc.preferredContentSizeCategory = UIContentSizeCategoryUnspecified;
tc.layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified;
}
#if AS_BUILD_UIUSERINTERFACESTYLE
if (AS_AVAILABLE_IOS_TVOS(12, 10)) {
tc.userInterfaceStyle = UIUserInterfaceStyleUnspecified;
}
#endif
return tc;
}
ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection) {
ASPrimitiveTraitCollection environmentTraitCollection = ASPrimitiveTraitCollectionMakeDefault();
environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass;
environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass;
environmentTraitCollection.displayScale = traitCollection.displayScale;
environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom;
environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability;
if (AS_AVAILABLE_IOS(10)) {
environmentTraitCollection.displayGamut = traitCollection.displayGamut;
environmentTraitCollection.layoutDirection = traitCollection.layoutDirection;
ASDisplayNodeCAssertPermanent(traitCollection.preferredContentSizeCategory);
environmentTraitCollection.preferredContentSizeCategory = traitCollection.preferredContentSizeCategory;
}
#if AS_BUILD_UIUSERINTERFACESTYLE
if (AS_AVAILABLE_IOS_TVOS(12, 10)) {
environmentTraitCollection.userInterfaceStyle = traitCollection.userInterfaceStyle;
}
#endif
return environmentTraitCollection;
}
BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs) {
return !memcmp(&lhs, &rhs, sizeof(ASPrimitiveTraitCollection));
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) {
switch (idiom) {
case UIUserInterfaceIdiomTV:
return @"TV";
case UIUserInterfaceIdiomPad:
return @"Pad";
case UIUserInterfaceIdiomPhone:
return @"Phone";
case UIUserInterfaceIdiomCarPlay:
return @"CarPlay";
default:
return @"Unspecified";
}
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIForceTouchCapability(UIForceTouchCapability capability) {
switch (capability) {
case UIForceTouchCapabilityAvailable:
return @"Available";
case UIForceTouchCapabilityUnavailable:
return @"Unavailable";
default:
return @"Unknown";
}
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceSizeClass(UIUserInterfaceSizeClass sizeClass) {
switch (sizeClass) {
case UIUserInterfaceSizeClassCompact:
return @"Compact";
case UIUserInterfaceSizeClassRegular:
return @"Regular";
default:
return @"Unspecified";
}
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
API_AVAILABLE(ios(10))
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIDisplayGamut(UIDisplayGamut displayGamut) {
switch (displayGamut) {
case UIDisplayGamutSRGB:
return @"sRGB";
case UIDisplayGamutP3:
return @"P3";
default:
return @"Unspecified";
}
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
API_AVAILABLE(ios(10))
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUITraitEnvironmentLayoutDirection(UITraitEnvironmentLayoutDirection layoutDirection) {
switch (layoutDirection) {
case UITraitEnvironmentLayoutDirectionLeftToRight:
return @"LeftToRight";
case UITraitEnvironmentLayoutDirectionRightToLeft:
return @"RightToLeft";
default:
return @"Unspecified";
}
}
// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline
#if AS_BUILD_UIUSERINTERFACESTYLE
API_AVAILABLE(tvos(10.0), ios(12.0))
ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceStyle(UIUserInterfaceStyle userInterfaceStyle) {
switch (userInterfaceStyle) {
case UIUserInterfaceStyleLight:
return @"Light";
case UIUserInterfaceStyleDark:
return @"Dark";
default:
return @"Unspecified";
}
}
#endif
NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits) {
NSMutableArray<NSDictionary *> *props = [NSMutableArray array];
[props addObject:@{ @"verticalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.verticalSizeClass) }];
[props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }];
[props addObject:@{ @"displayScale": [NSString stringWithFormat: @"%.0lf", (double)traits.displayScale] }];
[props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }];
[props addObject:@{ @"forceTouchCapability": AS_NSStringFromUIForceTouchCapability(traits.forceTouchCapability) }];
#if AS_BUILD_UIUSERINTERFACESTYLE
if (AS_AVAILABLE_IOS_TVOS(12, 10)) {
[props addObject:@{ @"userInterfaceStyle": AS_NSStringFromUIUserInterfaceStyle(traits.userInterfaceStyle) }];
}
#endif
if (AS_AVAILABLE_IOS(10)) {
[props addObject:@{ @"layoutDirection": AS_NSStringFromUITraitEnvironmentLayoutDirection(traits.layoutDirection) }];
[props addObject:@{ @"preferredContentSizeCategory": traits.preferredContentSizeCategory }];
[props addObject:@{ @"displayGamut": AS_NSStringFromUIDisplayGamut(traits.displayGamut) }];
}
[props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }];
return ASObjectDescriptionMakeWithoutObject(props);
}
#pragma mark - ASTraitCollection
@implementation ASTraitCollection {
ASPrimitiveTraitCollection _prim;
}
+ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED {
ASTraitCollection *tc = [[ASTraitCollection alloc] init];
if (AS_AVAILABLE_IOS(10)) {
ASDisplayNodeCAssertPermanent(traits.preferredContentSizeCategory);
}
tc->_prim = traits;
return tc;
}
- (ASPrimitiveTraitCollection)primitiveTraitCollection {
return _prim;
}
- (UIUserInterfaceSizeClass)horizontalSizeClass
{
return _prim.horizontalSizeClass;
}
-(UIUserInterfaceSizeClass)verticalSizeClass
{
return _prim.verticalSizeClass;
}
- (CGFloat)displayScale
{
return _prim.displayScale;
}
- (UIDisplayGamut)displayGamut
{
return _prim.displayGamut;
}
- (UIForceTouchCapability)forceTouchCapability
{
return _prim.forceTouchCapability;
}
- (UITraitEnvironmentLayoutDirection)layoutDirection
{
return _prim.layoutDirection;
}
- (CGSize)containerSize
{
return _prim.containerSize;
}
#if AS_BUILD_UIUSERINTERFACESTYLE
- (UIUserInterfaceStyle)userInterfaceStyle
{
return _prim.userInterfaceStyle;
}
#endif
- (UIContentSizeCategory)preferredContentSizeCategory
{
return _prim.preferredContentSizeCategory;
}
- (NSUInteger)hash {
return ASHashBytes(&_prim, sizeof(ASPrimitiveTraitCollection));
}
- (BOOL)isEqual:(id)object {
if (!object || ![object isKindOfClass:ASTraitCollection.class]) {
return NO;
}
return [self isEqualToTraitCollection:object];
}
- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection
{
if (traitCollection == nil) {
return NO;
}
if (self == traitCollection) {
return YES;
}
return ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(_prim, traitCollection->_prim);
}
@end

View File

@ -0,0 +1,59 @@
//
// ASWeakMap.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
NS_ASSUME_NONNULL_BEGIN
/**
* This class is used in conjunction with ASWeakMap. Instances of this type are returned by an ASWeakMap,
* must retain this value for as long as they want the entry to exist in the map.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASWeakMapEntry<Value> : NSObject
@property (readonly) Value value;
@end
/**
* This is not a full-featured map. It does not support features like `count` and FastEnumeration because there
* is not currently a need.
*
* This is a map that does not retain keys or values added to it. When both getting and setting, the caller is
* returned a ASWeakMapEntry and must retain it for as long as it wishes the key/value to remain in the map.
* We return a single Entry value to the caller to avoid two potential problems:
*
* 1) It's easier for callers to retain one value (the Entry) and not two (a key and a value).
* 2) Key values are tested for `isEqual` equality. If if a caller asks for a key "A" that is equal to a key "B"
* already in the map, then we need the caller to retain key "B" and not key "A". Returning an Entry simplifies
* the semantics.
*
* The underlying storage is a hash table and the Key type should implement `hash` and `isEqual:`.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASWeakMap<__covariant Key, Value> : NSObject
/**
* Read from the cache. The Value object is accessible from the returned ASWeakMapEntry.
*/
- (nullable ASWeakMapEntry<Value> *)entryForKey:(Key)key AS_WARN_UNUSED_RESULT;
/**
* Put a value into the cache. If an entry with an equal key already exists, then the value is updated on the existing entry.
*/
- (ASWeakMapEntry<Value> *)setObject:(Value)value forKey:(Key)key AS_WARN_UNUSED_RESULT;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,78 @@
//
// ASWeakMap.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASWeakMap.h"
@interface ASWeakMapEntry ()
@property (nonatomic, readonly) id key;
@property id value;
@end
@implementation ASWeakMapEntry
- (instancetype)initWithKey:(id)key value:(id)value
{
self = [super init];
if (self) {
_key = key;
_value = value;
}
return self;
}
@end
@interface ASWeakMap ()
@property (nonatomic, readonly) NSMapTable<id, ASWeakMapEntry *> *hashTable;
@end
/**
* Implementation details:
*
* The retained size of our keys is potentially very large (for example, a UIImage is commonly part of a key).
* Unfortunately, NSMapTable does not make guarantees about how quickly it will dispose of entries where
* either the key or the value is weak and has been disposed. So, a NSMapTable with "strong key to weak value" is
* unsuitable for our purpose because the strong keys are retained longer than the value and for an indefininte period of time.
* More details here: http://cocoamine.net/blog/2013/12/13/nsmaptable-and-zeroing-weak-references/
*
* Our NSMapTable is "weak key to weak value" where each key maps to an Entry. The Entry object is responsible
* for retaining both the key and value. Our convention is that the caller must retain the Entry object
* in order to keep the key and the value in the cache.
*/
@implementation ASWeakMap
- (instancetype)init
{
self = [super init];
if (self) {
_hashTable = [NSMapTable weakToWeakObjectsMapTable];
}
return self;
}
- (ASWeakMapEntry *)entryForKey:(id)key
{
return [self.hashTable objectForKey:key];
}
- (ASWeakMapEntry *)setObject:(id)value forKey:(id)key
{
ASWeakMapEntry *entry = [self.hashTable objectForKey:key];
if (entry != nil) {
// Update the value in the existing entry.
entry.value = value;
} else {
entry = [[ASWeakMapEntry alloc] initWithKey:key value:value];
[self.hashTable setObject:entry forKey:key];
}
return entry;
}
@end

View File

@ -0,0 +1,32 @@
//
// ASWeakProxy.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
AS_SUBCLASSING_RESTRICTED
@interface ASWeakProxy : NSProxy
/**
* @return target The target which will be forwarded all messages sent to the weak proxy.
*/
@property (nonatomic, weak, readonly) id target;
/**
* An object which forwards messages to a target which it weakly references
*
* @discussion This class is useful for breaking retain cycles. You can pass this in place
* of the target to something which creates a strong reference. All messages sent to the
* proxy will be passed onto the target.
*
* @return an instance of ASWeakProxy
*/
+ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED;
@end

View File

@ -0,0 +1,72 @@
//
// ASWeakProxy.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASWeakProxy.h"
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASAssert.h>
@implementation ASWeakProxy
- (instancetype)initWithTarget:(id)target
{
if (self) {
_target = target;
}
return self;
}
+ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED
{
return [[ASWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return _target;
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
return [_target respondsToSelector:aSelector];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol
{
return [_target conformsToProtocol:aProtocol];
}
/// Strangely, this method doesn't get forwarded by ObjC.
- (BOOL)isKindOfClass:(Class)aClass
{
return [_target isKindOfClass:aClass];
}
- (NSString *)description
{
return ASObjectDescriptionMake(self, @[@{ @"target": _target ?: (id)kCFNull }]);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd));
// Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature
// from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found.
// This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method
// returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines
// the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will
// suffice. Since the -forwardInvocation call will do nothing if the target does not respond to the selector,
// the dud NSMethodSignature simply gets us around the exception.
return [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd));
}
@end

View File

@ -0,0 +1,84 @@
//
// ASWeakSet.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASWeakSet.h>
@interface ASWeakSet<__covariant ObjectType> ()
@property (nonatomic, readonly) NSHashTable<ObjectType> *hashTable;
@end
@implementation ASWeakSet
- (instancetype)init
{
self = [super init];
if (self) {
_hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality];
}
return self;
}
- (void)addObject:(id)object
{
[_hashTable addObject:object];
}
- (void)removeObject:(id)object
{
[_hashTable removeObject:object];
}
- (void)removeAllObjects
{
[_hashTable removeAllObjects];
}
- (NSArray *)allObjects
{
return _hashTable.allObjects;
}
- (BOOL)containsObject:(id)object
{
return [_hashTable containsObject:object];
}
- (BOOL)isEmpty
{
return [_hashTable anyObject] == nil;
}
/**
Note: The `count` property of NSHashTable is unreliable
in the case of weak-memory hash tables because entries
that have been deallocated are not removed immediately.
In order to get the true count we have to fall back to using
fast enumeration.
*/
- (NSUInteger)count
{
NSUInteger count = 0;
for (__unused id object in _hashTable) {
count += 1;
}
return count;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id _Nonnull *)buffer count:(NSUInteger)len
{
return [_hashTable countByEnumeratingWithState:state objects:buffer count:len];
}
- (NSString *)description
{
return [[super description] stringByAppendingFormat:@" count: %tu, contents: %@", self.count, _hashTable];
}
@end

View File

@ -0,0 +1,177 @@
//
// NSArray+Diffing.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/NSArray+Diffing.h>
#import <UIKit/NSIndexPath+UIKitAdditions.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <unordered_map>
@implementation NSArray (Diffing)
typedef BOOL (^compareBlock)(id _Nonnull lhs, id _Nonnull rhs);
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
{
[self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:[NSArray defaultCompareBlock]];
}
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
compareBlock:(compareBlock)comparison
{
[self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:comparison];
}
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
moves:(NSArray<NSIndexPath *> **)moves
{
[self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:moves
compareBlock:[NSArray defaultCompareBlock]];
}
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
moves:(NSArray<NSIndexPath *> **)moves compareBlock:(compareBlock)comparison
{
struct NSObjectHash
{
std::size_t operator()(id <NSObject> k) const { return (std::size_t) [k hash]; };
};
struct NSObjectCompare
{
bool operator()(id <NSObject> lhs, id <NSObject> rhs) const { return (bool) [lhs isEqual:rhs]; };
};
std::unordered_multimap<unowned id, NSUInteger, NSObjectHash, NSObjectCompare> potentialMoves;
NSAssert(comparison != nil, @"Comparison block is required");
NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)");
NSMutableArray<NSIndexPath *> *moveIndexPaths = nil;
NSMutableIndexSet *insertionIndexes = nil, *deletionIndexes = nil;
if (moves) {
moveIndexPaths = [NSMutableArray new];
}
NSMutableIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison];
if (deletions || moves) {
deletionIndexes = [NSMutableIndexSet indexSet];
NSUInteger i = 0;
for (id element in self) {
if (![commonIndexes containsIndex:i]) {
[deletionIndexes addIndex:i];
}
if (moves) {
potentialMoves.insert(std::pair<id, NSUInteger>(element, i));
}
++i;
}
}
if (insertions || moves) {
insertionIndexes = [NSMutableIndexSet indexSet];
NSArray *commonObjects = [self objectsAtIndexes:commonIndexes];
for (NSUInteger i = 0, j = 0; j < array.count; j++) {
auto moveFound = potentialMoves.find(array[j]);
NSUInteger movedFrom = NSNotFound;
if (moveFound != potentialMoves.end() && moveFound->second != j) {
movedFrom = moveFound->second;
potentialMoves.erase(moveFound);
[moveIndexPaths addObject:[NSIndexPath indexPathForItem:j inSection:movedFrom]];
}
if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) {
i++;
} else {
if (movedFrom != NSNotFound) {
// moves will coalesce a delete / insert - the insert is just not done, and here we remove the delete:
[deletionIndexes removeIndex:movedFrom];
// OR a move will have come from the LCS:
if ([commonIndexes containsIndex:movedFrom]) {
[commonIndexes removeIndex:movedFrom];
commonObjects = [self objectsAtIndexes:commonIndexes];
}
} else {
[insertionIndexes addIndex:j];
}
}
}
}
if (moves) {*moves = moveIndexPaths;}
if (deletions) {*deletions = deletionIndexes;}
if (insertions) {*insertions = insertionIndexes;}
}
// https://github.com/raywenderlich/swift-algorithm-club/tree/master/Longest%20Common%20Subsequence is not exactly this code (obviously), but
// is a good commentary on the algorithm.
- (NSMutableIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison
{
NSAssert(comparison != nil, @"Comparison block is required");
NSInteger selfCount = self.count;
NSInteger arrayCount = array.count;
// Allocate the diff map in the heap so we don't blow the stack for large arrays.
NSInteger **lengths = NULL;
lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1));
if (lengths == NULL) {
ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing");
return nil;
}
// Fill in a LCS length matrix:
for (NSInteger i = 0; i <= selfCount; i++) {
lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1));
if (lengths[i] == NULL) {
ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing");
return nil;
}
id selfObj = i > 0 ? self[i-1] : nil;
for (NSInteger j = 0; j <= arrayCount; j++) {
if (i == 0 || j == 0) {
lengths[i][j] = 0;
} else if (comparison(selfObj, array[j-1])) {
lengths[i][j] = 1 + lengths[i-1][j-1];
} else {
lengths[i][j] = MAX(lengths[i-1][j], lengths[i][j-1]);
}
}
}
// Backtrack to fill in indices based on length matrix:
NSMutableIndexSet *common = [NSMutableIndexSet indexSet];
NSInteger i = selfCount, j = arrayCount;
while(i > 0 && j > 0) {
if (comparison(self[i-1], array[j-1])) {
[common addIndex:(i-1)];
i--; j--;
} else if (lengths[i-1][j] > lengths[i][j-1]) {
i--;
} else {
j--;
}
}
for (NSInteger i = 0; i <= selfCount; i++) {
free(lengths[i]);
}
free(lengths);
return common;
}
static compareBlock defaultCompare = nil;
+ (compareBlock)defaultCompareBlock
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultCompare = ^BOOL(id lhs, id rhs) {
return [lhs isEqual:rhs];
};
});
return defaultCompare;
}
@end

View File

@ -0,0 +1,29 @@
//
// NSIndexSet+ASHelpers.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
@interface NSIndexSet (ASHelpers)
- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger idx))block;
- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes;
/// Returns all the item indexes from the given index paths that are in the given section.
+ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray<NSIndexPath *> *)indexPaths inSection:(NSUInteger)section;
/// If you've got an old index, and you insert items using this index set, this returns the change to get to the new index.
- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index;
- (NSString *)as_smallDescription;
/// Returns all the section indexes contained in the index paths array.
+ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
@end

View File

@ -0,0 +1,91 @@
//
// NSIndexSet+ASHelpers.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
// UIKit indexPath helpers
#import <UIKit/UIKit.h>
#import "NSIndexSet+ASHelpers.h"
@implementation NSIndexSet (ASHelpers)
- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block
{
NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init];
[self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
NSUInteger newIndex = block(i);
if (newIndex != NSNotFound) {
[result addIndex:newIndex];
}
}
}];
return result;
}
- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes
{
NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init];
[self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
[indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) {
[result addIndexesInRange:range];
}];
}];
return result;
}
+ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray<NSIndexPath *> *)indexPaths inSection:(NSUInteger)section
{
NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *indexPath in indexPaths) {
if (indexPath.section == section) {
[result addIndex:indexPath.item];
}
}
return result;
}
- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index
{
__block NSUInteger newIndex = index;
[self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
for (NSUInteger i = range.location; i < NSMaxRange(range); i++) {
if (i <= newIndex) {
newIndex += 1;
} else {
*stop = YES;
}
}
}];
return newIndex - index;
}
- (NSString *)as_smallDescription
{
NSMutableString *result = [NSMutableString stringWithString:@"{ "];
[self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
if (range.length == 1) {
[result appendFormat:@"%tu ", range.location];
} else {
[result appendFormat:@"%tu-%tu ", range.location, NSMaxRange(range) - 1];
}
}];
[result appendString:@"}"];
return result;
}
+ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
{
NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *indexPath in indexPaths) {
[result addIndex:indexPath.section];
}
return result;
}
@end

View File

@ -989,4 +989,8 @@ typedef NS_ENUM(NSInteger, ASLayoutEngineType) {
@property (nullable, weak) ASDisplayNode *asyncdisplaykit_node;
@end
@interface CALayer (ASDisplayNodeInternal)
@property (nullable, weak) ASDisplayNode *asyncdisplaykit_node;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,32 @@
//
// UIResponder+AsyncDisplayKit.m
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/UIResponder+AsyncDisplayKit.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import "ASResponderChainEnumerator.h"
@implementation UIResponder (AsyncDisplayKit)
- (__kindof UIViewController *)asdk_associatedViewController
{
ASDisplayNodeAssertMainThread();
for (UIResponder *responder in [self asdk_responderChainEnumerator]) {
UIViewController *vc = ASDynamicCast(responder, UIViewController);
if (vc) {
return vc;
}
}
return nil;
}
@end

View File

@ -0,0 +1,465 @@
//
// _ASAsyncTransaction.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASAsyncTransactionGroup.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASThread.h>
#import <list>
#import <map>
#import <mutex>
#ifndef __STRICT_ANSI__
#warning "Texture must be compiled with std=c++11 to prevent layout issues. gnu++ is not supported. This is hopefully temporary."
#endif
AS_EXTERN NSRunLoopMode const UITrackingRunLoopMode;
NSInteger const ASDefaultTransactionPriority = 0;
@interface ASAsyncTransactionOperation : NSObject
- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock;
@property (nonatomic) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock;
@property id value; // set on bg queue by the operation block
@end
@implementation ASAsyncTransactionOperation
- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock
{
if ((self = [super init])) {
_operationCompletionBlock = operationCompletionBlock;
}
return self;
}
- (void)dealloc
{
NSAssert(_operationCompletionBlock == nil, @"Should have been called and released before -dealloc");
}
- (void)callAndReleaseCompletionBlock:(BOOL)canceled;
{
ASDisplayNodeAssertMainThread();
if (_operationCompletionBlock) {
_operationCompletionBlock(self.value, canceled);
// Guarantee that _operationCompletionBlock is released on main thread
_operationCompletionBlock = nil;
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<ASAsyncTransactionOperation: %p - value = %@>", self, self.value];
}
@end
// Lightweight operation queue for _ASAsyncTransaction that limits number of spawned threads
class ASAsyncTransactionQueue
{
public:
// Similar to dispatch_group_t
class Group
{
public:
// call when group is no longer needed; after last scheduled operation the group will delete itself
virtual void release() = 0;
// schedule block on given queue
virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0;
// dispatch block on given queue when all previously scheduled blocks finished executing
virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0;
// used when manually executing blocks
virtual void enter() = 0;
virtual void leave() = 0;
// wait until all scheduled blocks finished executing
virtual void wait() = 0;
protected:
virtual ~Group() { }; // call release() instead
};
// Create new group
Group *createGroup();
static ASAsyncTransactionQueue &instance();
private:
struct GroupNotify
{
dispatch_block_t _block;
dispatch_queue_t _queue;
};
class GroupImpl : public Group
{
public:
GroupImpl(ASAsyncTransactionQueue &queue)
: _pendingOperations(0)
, _releaseCalled(false)
, _queue(queue)
{
}
virtual void release();
virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block);
virtual void notify(dispatch_queue_t queue, dispatch_block_t block);
virtual void enter();
virtual void leave();
virtual void wait();
int _pendingOperations;
std::list<GroupNotify> _notifyList;
std::condition_variable _condition;
BOOL _releaseCalled;
ASAsyncTransactionQueue &_queue;
};
struct Operation
{
dispatch_block_t _block;
GroupImpl *_group;
NSInteger _priority;
};
struct DispatchEntry // entry for each dispatch queue
{
typedef std::list<Operation> OperationQueue;
typedef std::list<OperationQueue::iterator> OperationIteratorList; // each item points to operation queue
typedef std::map<NSInteger, OperationIteratorList> OperationPriorityMap; // sorted by priority
OperationQueue _operationQueue;
OperationPriorityMap _operationPriorityMap;
int _threadCount;
Operation popNextOperation(bool respectPriority); // assumes locked mutex
void pushOperation(Operation operation); // assumes locked mutex
};
std::map<dispatch_queue_t, DispatchEntry> _entries;
std::mutex _mutex;
};
ASAsyncTransactionQueue::Group* ASAsyncTransactionQueue::createGroup()
{
Group *res = new GroupImpl(*this);
return res;
}
void ASAsyncTransactionQueue::GroupImpl::release()
{
std::lock_guard<std::mutex> l(_queue._mutex);
if (_pendingOperations == 0) {
delete this;
} else {
_releaseCalled = YES;
}
}
ASAsyncTransactionQueue::Operation ASAsyncTransactionQueue::DispatchEntry::popNextOperation(bool respectPriority)
{
NSCAssert(!_operationQueue.empty() && !_operationPriorityMap.empty(), @"No scheduled operations available");
OperationQueue::iterator queueIterator;
OperationPriorityMap::iterator mapIterator;
if (respectPriority) {
mapIterator = --_operationPriorityMap.end(); // highest priority "bucket"
queueIterator = *mapIterator->second.begin();
} else {
queueIterator = _operationQueue.begin();
mapIterator = _operationPriorityMap.find(queueIterator->_priority);
}
// no matter what, first item in "bucket" must match item in queue
NSCAssert(mapIterator->second.front() == queueIterator, @"Queue inconsistency");
Operation res = *queueIterator;
_operationQueue.erase(queueIterator);
mapIterator->second.pop_front();
if (mapIterator->second.empty()) {
_operationPriorityMap.erase(mapIterator);
}
return res;
}
void ASAsyncTransactionQueue::DispatchEntry::pushOperation(ASAsyncTransactionQueue::Operation operation)
{
_operationQueue.push_back(operation);
OperationIteratorList &list = _operationPriorityMap[operation._priority];
list.push_back(--_operationQueue.end());
}
void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block)
{
ASAsyncTransactionQueue &q = _queue;
std::lock_guard<std::mutex> l(q._mutex);
DispatchEntry &entry = q._entries[queue];
Operation operation;
operation._block = block;
operation._group = this;
operation._priority = priority;
entry.pushOperation(operation);
++_pendingOperations; // enter group
#if ASDISPLAYNODE_DELAY_DISPLAY
NSUInteger maxThreads = 1;
#else
NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2;
// Bit questionable maybe - we can give main thread more CPU time during tracking.
if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode])
--maxThreads;
#endif
if (entry._threadCount < maxThreads) { // we need to spawn another thread
// first thread will take operations in queue order (regardless of priority), other threads will respect priority
bool respectPriority = entry._threadCount > 0;
++entry._threadCount;
dispatch_async(queue, ^{
std::unique_lock<std::mutex> lock(q._mutex);
// go until there are no more pending operations
while (!entry._operationQueue.empty()) {
Operation operation = entry.popNextOperation(respectPriority);
lock.unlock();
if (operation._block) {
operation._block();
}
operation._group->leave();
operation._block = nil; // the block must be freed while mutex is unlocked
lock.lock();
}
--entry._threadCount;
if (entry._threadCount == 0) {
NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen
q._entries.erase(queue);
}
});
}
}
void ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t queue, dispatch_block_t block)
{
std::lock_guard<std::mutex> l(_queue._mutex);
if (_pendingOperations == 0) {
dispatch_async(queue, block);
} else {
GroupNotify notify;
notify._block = block;
notify._queue = queue;
_notifyList.push_back(notify);
}
}
void ASAsyncTransactionQueue::GroupImpl::enter()
{
std::lock_guard<std::mutex> l(_queue._mutex);
++_pendingOperations;
}
void ASAsyncTransactionQueue::GroupImpl::leave()
{
std::lock_guard<std::mutex> l(_queue._mutex);
--_pendingOperations;
if (_pendingOperations == 0) {
std::list<GroupNotify> notifyList;
_notifyList.swap(notifyList);
for (GroupNotify & notify : notifyList) {
dispatch_async(notify._queue, notify._block);
}
_condition.notify_one();
// there was attempt to release the group before, but we still
// had operations scheduled so now is good time
if (_releaseCalled) {
delete this;
}
}
}
void ASAsyncTransactionQueue::GroupImpl::wait()
{
std::unique_lock<std::mutex> lock(_queue._mutex);
while (_pendingOperations > 0) {
_condition.wait(lock);
}
}
ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance()
{
static ASAsyncTransactionQueue *instance = new ASAsyncTransactionQueue();
return *instance;
}
@interface _ASAsyncTransaction ()
@property ASAsyncTransactionState state;
@end
@implementation _ASAsyncTransaction
{
ASAsyncTransactionQueue::Group *_group;
NSMutableArray<ASAsyncTransactionOperation *> *_operations;
}
#pragma mark - Lifecycle
- (instancetype)initWithCompletionBlock:(void(^)(_ASAsyncTransaction *, BOOL))completionBlock
{
if ((self = [self init])) {
_completionBlock = completionBlock;
self.state = ASAsyncTransactionStateOpen;
}
return self;
}
- (void)dealloc
{
// Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue.
NSAssert(self.state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed");
if (_group) {
_group->release();
}
}
#pragma mark - Transaction Management
- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block
priority:(NSInteger)priority
queue:(dispatch_queue_t)queue
completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion
{
ASDisplayNodeAssertMainThread();
NSAssert(self.state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions");
[self _ensureTransactionData];
ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion];
[_operations addObject:operation];
_group->schedule(priority, queue, ^{
@autoreleasepool {
if (self.state != ASAsyncTransactionStateCanceled) {
operation.value = block();
}
}
});
}
- (void)cancel
{
ASDisplayNodeAssertMainThread();
NSAssert(self.state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction");
self.state = ASAsyncTransactionStateCanceled;
}
- (void)commit
{
ASDisplayNodeAssertMainThread();
NSAssert(self.state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction");
self.state = ASAsyncTransactionStateCommitted;
if ([_operations count] == 0) {
// Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously.
if (_completionBlock) {
_completionBlock(self, NO);
}
} else {
NSAssert(_group != NULL, @"If there are operations, dispatch group should have been created");
_group->notify(dispatch_get_main_queue(), ^{
[self completeTransaction];
});
}
}
- (void)completeTransaction
{
ASDisplayNodeAssertMainThread();
ASAsyncTransactionState state = self.state;
if (state != ASAsyncTransactionStateComplete) {
BOOL isCanceled = (state == ASAsyncTransactionStateCanceled);
for (ASAsyncTransactionOperation *operation in _operations) {
[operation callAndReleaseCompletionBlock:isCanceled];
}
// Always set state to Complete, even if we were cancelled, to block any extraneous
// calls to this method that may have been scheduled for the next runloop
// (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled)
self.state = ASAsyncTransactionStateComplete;
if (_completionBlock) {
_completionBlock(self, isCanceled);
}
}
}
- (void)waitUntilComplete
{
ASDisplayNodeAssertMainThread();
if (self.state != ASAsyncTransactionStateComplete) {
if (_group) {
_group->wait();
// At this point, the asynchronous operation may have completed, but the runloop
// observer has not committed the batch of transactions we belong to. It's important to
// commit ourselves via the group to avoid double-committing the transaction.
// This is only necessary when forcing display work to complete before allowing the runloop
// to continue, e.g. in the implementation of -[ASDisplayNode recursivelyEnsureDisplay].
if (self.state == ASAsyncTransactionStateOpen) {
[_ASAsyncTransactionGroup.mainTransactionGroup commit];
NSAssert(self.state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group");
}
// If we needed to commit the group above, -completeTransaction may have already been run.
// It is designed to accommodate this by checking _state to ensure it is not complete.
[self completeTransaction];
}
}
}
#pragma mark - Helper Methods
- (void)_ensureTransactionData
{
// Lazily initialize _group and _operations to avoid overhead in the case where no operations are added to the transaction
if (_group == NULL) {
_group = ASAsyncTransactionQueue::instance().createGroup();
}
if (_operations == nil) {
_operations = [[NSMutableArray alloc] init];
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)self.state, _group, _operations];
}
@end

View File

@ -0,0 +1,24 @@
//
// _ASAsyncTransactionContainer+Private.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
NS_ASSUME_NONNULL_BEGIN
@class _ASAsyncTransaction;
@interface CALayer (ASAsyncTransactionContainerTransactions)
@property (nonatomic, nullable, setter=asyncdisplaykit_setAsyncLayerTransactions:) NSHashTable<_ASAsyncTransaction *> *asyncdisplaykit_asyncLayerTransactions;
- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction;
- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,121 @@
//
// _ASAsyncTransactionContainer.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASAsyncTransactionContainer.h>
#import "_ASAsyncTransactionContainer+Private.h"
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASAsyncTransactionGroup.h>
@implementation CALayer (ASAsyncTransactionContainerTransactions)
@dynamic asyncdisplaykit_asyncLayerTransactions;
// No-ops in the base class. Mostly exposed for testing.
- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction {}
- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction {}
@end
@implementation CALayer (ASAsyncTransactionContainer)
@dynamic asyncdisplaykit_currentAsyncTransaction;
@dynamic asyncdisplaykit_asyncTransactionContainer;
- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState
{
return ([self.asyncdisplaykit_asyncLayerTransactions count] == 0) ? ASAsyncTransactionContainerStateNoTransactions : ASAsyncTransactionContainerStatePendingTransactions;
}
- (void)asyncdisplaykit_cancelAsyncTransactions
{
// If there was an open transaction, commit and clear the current transaction. Otherwise:
// (1) The run loop observer will try to commit a canceled transaction which is not allowed
// (2) We leave the canceled transaction attached to the layer, dooming future operations
_ASAsyncTransaction *currentTransaction = self.asyncdisplaykit_currentAsyncTransaction;
[currentTransaction commit];
self.asyncdisplaykit_currentAsyncTransaction = nil;
for (_ASAsyncTransaction *transaction in [self.asyncdisplaykit_asyncLayerTransactions copy]) {
[transaction cancel];
}
}
- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction
{
_ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncTransaction;
if (transaction == nil) {
NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions;
if (transactions == nil) {
transactions = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
self.asyncdisplaykit_asyncLayerTransactions = transactions;
}
__weak CALayer *weakSelf = self;
transaction = [[_ASAsyncTransaction alloc] initWithCompletionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) {
__strong CALayer *self = weakSelf;
if (self == nil) {
return;
}
[transactions removeObject:completedTransaction];
[self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction];
}];
[transactions addObject:transaction];
self.asyncdisplaykit_currentAsyncTransaction = transaction;
[self asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:transaction];
}
[_ASAsyncTransactionGroup.mainTransactionGroup addTransactionContainer:self];
return transaction;
}
- (CALayer *)asyncdisplaykit_parentTransactionContainer
{
CALayer *containerLayer = self;
while (containerLayer && !containerLayer.asyncdisplaykit_isAsyncTransactionContainer) {
containerLayer = containerLayer.superlayer;
}
return containerLayer;
}
@end
@implementation UIView (ASAsyncTransactionContainer)
- (BOOL)asyncdisplaykit_isAsyncTransactionContainer
{
return self.layer.asyncdisplaykit_isAsyncTransactionContainer;
}
- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer
{
self.layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer;
}
- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState
{
return self.layer.asyncdisplaykit_asyncTransactionContainerState;
}
- (void)asyncdisplaykit_cancelAsyncTransactions
{
[self.layer asyncdisplaykit_cancelAsyncTransactions];
}
- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange
{
// No-op in the base class.
}
- (void)asyncdisplaykit_setCurrentAsyncTransaction:(_ASAsyncTransaction *)transaction
{
self.layer.asyncdisplaykit_currentAsyncTransaction = transaction;
}
- (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction
{
return self.layer.asyncdisplaykit_currentAsyncTransaction;
}
@end

View File

@ -0,0 +1,88 @@
//
// _ASAsyncTransactionGroup.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASAsyncTransactionGroup.h>
#import <AsyncDisplayKit/_ASAsyncTransactionContainer.h>
#import "_ASAsyncTransactionContainer+Private.h"
@implementation _ASAsyncTransactionGroup {
NSHashTable<id<ASAsyncTransactionContainer>> *_containers;
}
+ (_ASAsyncTransactionGroup *)mainTransactionGroup
{
ASDisplayNodeAssertMainThread();
static _ASAsyncTransactionGroup *mainTransactionGroup;
if (mainTransactionGroup == nil) {
mainTransactionGroup = [[_ASAsyncTransactionGroup alloc] _init];
[mainTransactionGroup registerAsMainRunloopObserver];
}
return mainTransactionGroup;
}
- (void)registerAsMainRunloopObserver
{
ASDisplayNodeAssertMainThread();
static CFRunLoopObserverRef observer;
ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice");
// defer the commit of the transaction so we can add more during the current runloop iteration
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
kCFRunLoopExit); // before exiting a runloop run
observer = CFRunLoopObserverCreateWithHandler(NULL, // allocator
activities, // activities
YES, // repeats
INT_MAX, // order after CA transaction commits
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
ASDisplayNodeCAssertMainThread();
[self commit];
});
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
- (instancetype)_init
{
if ((self = [super init])) {
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
}
return self;
}
- (void)addTransactionContainer:(id<ASAsyncTransactionContainer>)container
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(container != nil, @"No container");
[_containers addObject:container];
}
- (void)commit
{
ASDisplayNodeAssertMainThread();
if ([_containers count]) {
NSHashTable *containersToCommit = _containers;
_containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality];
for (id<ASAsyncTransactionContainer> container in containersToCommit) {
// Note that the act of committing a transaction may open a new transaction,
// so we must nil out the transaction we're committing first.
_ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction;
container.asyncdisplaykit_currentAsyncTransaction = nil;
[transaction commit];
}
}
}
@end

View File

@ -0,0 +1,187 @@
//
// _ASCoreAnimationExtras.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASAssert.h>
void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image)
{
ASDisplayNodeSetResizableContents(layer, image);
}
void ASDisplayNodeSetResizableContents(id<ASResizableContents> obj, UIImage *image)
{
// FIXME (https://github.com/TextureGroup/Texture/issues/1046): This method does not currently handle UIImageResizingModeTile, which is the default.
// See also https://developer.apple.com/documentation/uikit/uiimage/1624157-resizingmode?language=objc
// I'm not sure of a way to use CALayer directly to perform such tiling on the GPU, though the stretch is handled by the GPU,
// and CALayer.h documents the fact that contentsCenter is used to stretch the pixels.
if (image) {
ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero),
@"Image insets must be all-zero or resizingMode has to be UIImageResizingModeStretch. XCode assets default value is UIImageResizingModeTile which is not supported by Texture because of GPU-accelerated CALayer features.");
// Image may not actually be stretchable in one or both dimensions; this is handled
obj.contents = (id)[image CGImage];
obj.contentsScale = [image scale];
obj.rasterizationScale = [image scale];
CGSize imageSize = [image size];
UIEdgeInsets insets = [image capInsets];
// These are lifted from what UIImageView does by experimentation. Without these exact values, the stretching is slightly off.
const CGFloat halfPixelFudge = 0.49f;
const CGFloat otherPixelFudge = 0.02f;
// Convert to unit coordinates for the contentsCenter property.
CGRect contentsCenter = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
if (insets.left > 0 || insets.right > 0) {
contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width);
contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.f) + otherPixelFudge) / imageSize.width;
}
if (insets.top > 0 || insets.bottom > 0) {
contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height);
contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.f) + otherPixelFudge) / imageSize.height;
}
obj.contentsGravity = kCAGravityResize;
obj.contentsCenter = contentsCenter;
} else {
obj.contents = nil;
}
}
struct _UIContentModeStringLUTEntry {
UIViewContentMode contentMode;
NSString *const string;
};
static const _UIContentModeStringLUTEntry *UIContentModeCAGravityLUT(size_t *count)
{
// Initialize this in a function (instead of at file level) to avoid
// startup initialization time.
static const _UIContentModeStringLUTEntry sUIContentModeCAGravityLUT[] = {
{UIViewContentModeScaleToFill, kCAGravityResize},
{UIViewContentModeScaleAspectFit, kCAGravityResizeAspect},
{UIViewContentModeScaleAspectFill, kCAGravityResizeAspectFill},
{UIViewContentModeCenter, kCAGravityCenter},
{UIViewContentModeTop, kCAGravityBottom},
{UIViewContentModeBottom, kCAGravityTop},
{UIViewContentModeLeft, kCAGravityLeft},
{UIViewContentModeRight, kCAGravityRight},
{UIViewContentModeTopLeft, kCAGravityBottomLeft},
{UIViewContentModeTopRight, kCAGravityBottomRight},
{UIViewContentModeBottomLeft, kCAGravityTopLeft},
{UIViewContentModeBottomRight, kCAGravityTopRight},
};
*count = sizeof(sUIContentModeCAGravityLUT) / sizeof(sUIContentModeCAGravityLUT[0]);
return sUIContentModeCAGravityLUT;
}
static const _UIContentModeStringLUTEntry *UIContentModeDescriptionLUT(size_t *count)
{
// Initialize this in a function (instead of at file level) to avoid
// startup initialization time.
static const _UIContentModeStringLUTEntry sUIContentModeDescriptionLUT[] = {
{UIViewContentModeScaleToFill, @"scaleToFill"},
{UIViewContentModeScaleAspectFit, @"aspectFit"},
{UIViewContentModeScaleAspectFill, @"aspectFill"},
{UIViewContentModeRedraw, @"redraw"},
{UIViewContentModeCenter, @"center"},
{UIViewContentModeTop, @"top"},
{UIViewContentModeBottom, @"bottom"},
{UIViewContentModeLeft, @"left"},
{UIViewContentModeRight, @"right"},
{UIViewContentModeTopLeft, @"topLeft"},
{UIViewContentModeTopRight, @"topRight"},
{UIViewContentModeBottomLeft, @"bottomLeft"},
{UIViewContentModeBottomRight, @"bottomRight"},
};
*count = sizeof(sUIContentModeDescriptionLUT) / sizeof(sUIContentModeDescriptionLUT[0]);
return sUIContentModeDescriptionLUT;
}
NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode)
{
size_t lutSize;
const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize);
for (size_t i = 0; i < lutSize; ++i) {
if (lut[i].contentMode == contentMode) {
return lut[i].string;
}
}
return [NSString stringWithFormat:@"%d", (int)contentMode];
}
UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string)
{
size_t lutSize;
const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize);
for (size_t i = 0; i < lutSize; ++i) {
if (ASObjectIsEqual(lut[i].string, string)) {
return lut[i].contentMode;
}
}
return UIViewContentModeScaleToFill;
}
NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode)
{
size_t lutSize;
const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize);
for (size_t i = 0; i < lutSize; ++i) {
if (lut[i].contentMode == contentMode) {
return lut[i].string;
}
}
ASDisplayNodeCAssert(contentMode == UIViewContentModeRedraw, @"Encountered an unknown contentMode %ld. Is this a new version of iOS?", (long)contentMode);
// Redraw is ok to return nil.
return nil;
}
#define ContentModeCacheSize 10
UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity)
{
static int currentCacheIndex = 0;
static NSMutableArray *cachedStrings = [NSMutableArray arrayWithCapacity:ContentModeCacheSize];
static UIViewContentMode cachedModes[ContentModeCacheSize] = {};
NSInteger foundCacheIndex = [cachedStrings indexOfObjectIdenticalTo:contentsGravity];
if (foundCacheIndex != NSNotFound && foundCacheIndex < ContentModeCacheSize) {
return cachedModes[foundCacheIndex];
}
size_t lutSize;
const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize);
for (size_t i = 0; i < lutSize; ++i) {
if (ASObjectIsEqual(lut[i].string, contentsGravity)) {
UIViewContentMode foundContentMode = lut[i].contentMode;
if (currentCacheIndex < ContentModeCacheSize) {
// Cache the input value. This is almost always a different pointer than in our LUT and will frequently
// be the same value for an overwhelming majority of inputs.
[cachedStrings addObject:contentsGravity];
cachedModes[currentCacheIndex] = foundContentMode;
currentCacheIndex++;
}
return foundContentMode;
}
}
ASDisplayNodeCAssert(contentsGravity, @"Encountered an unknown contentsGravity \"%@\". Is this a new version of iOS?", contentsGravity);
ASDisplayNodeCAssert(!contentsGravity, @"You passed nil to ASDisplayNodeUIContentModeFromCAContentsGravity. We're falling back to resize, but this is probably a bug.");
// If asserts disabled, fall back to this
return UIViewContentModeScaleToFill;
}
BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer)
{
return (layer.animationKeys.count != 0);
}

View File

@ -0,0 +1,212 @@
//
// _ASDisplayLayer.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <objc/runtime.h>
#import <AsyncDisplayKit/_ASAsyncTransactionContainer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
@implementation _ASDisplayLayer
{
BOOL _attemptedDisplayWhileZeroSized;
struct {
BOOL delegateDidChangeBounds:1;
} _delegateFlags;
}
@dynamic displaysAsynchronously;
#ifdef DEBUG
- (void)dealloc {
if (![NSThread isMainThread]) {
assert(true);
}
}
#endif
#pragma mark - Properties
- (void)setDelegate:(id)delegate
{
[super setDelegate:delegate];
_delegateFlags.delegateDidChangeBounds = [delegate respondsToSelector:@selector(layer:didChangeBoundsWithOldValue:newValue:)];
}
- (void)setDisplaySuspended:(BOOL)displaySuspended
{
ASDisplayNodeAssertMainThread();
if (_displaySuspended != displaySuspended) {
_displaySuspended = displaySuspended;
if (!displaySuspended) {
// If resuming display, trigger a display now.
[self setNeedsDisplay];
} else {
// If suspending display, cancel any current async display so that we don't have contents set on us when it's finished.
[self cancelAsyncDisplay];
}
}
}
- (void)setBounds:(CGRect)bounds
{
BOOL valid = ASDisplayNodeAssertNonFatal(ASIsCGRectValidForLayout(bounds), @"Caught attempt to set invalid bounds %@ on %@.", NSStringFromCGRect(bounds), self);
if (!valid) {
return;
}
if (_delegateFlags.delegateDidChangeBounds) {
CGRect oldBounds = self.bounds;
[super setBounds:bounds];
self.asyncdisplaykit_node.threadSafeBounds = bounds;
[(id<ASCALayerExtendedDelegate>)self.delegate layer:self didChangeBoundsWithOldValue:oldBounds newValue:bounds];
} else {
[super setBounds:bounds];
self.asyncdisplaykit_node.threadSafeBounds = bounds;
}
if (_attemptedDisplayWhileZeroSized && CGRectIsEmpty(bounds) == NO && self.needsDisplayOnBoundsChange == NO) {
_attemptedDisplayWhileZeroSized = NO;
[self setNeedsDisplay];
}
}
#if DEBUG // These override is strictly to help detect application-level threading errors. Avoid method overhead in release.
- (void)setContents:(id)contents
{
ASDisplayNodeAssertMainThread();
[super setContents:contents];
}
- (void)setNeedsLayout
{
ASDisplayNodeAssertMainThread();
[super setNeedsLayout];
}
#endif
- (void)layoutSublayers
{
ASDisplayNodeAssertMainThread();
[super layoutSublayers];
[self.asyncdisplaykit_node __layout];
}
- (void)setNeedsDisplay
{
ASDisplayNodeAssertMainThread();
// FIXME: Reconsider whether we should cancel a display in progress.
// We should definitely cancel a display that is scheduled, but unstarted display.
[self cancelAsyncDisplay];
// Short circuit if display is suspended. When resumed, we will setNeedsDisplay at that time.
if (!_displaySuspended) {
[super setNeedsDisplay];
}
}
#pragma mark -
+ (dispatch_queue_t)displayQueue
{
static dispatch_queue_t displayQueue = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT);
// we use the highpri queue to prioritize UI rendering over other async operations
dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
});
return displayQueue;
}
+ (id)defaultValueForKey:(NSString *)key
{
if ([key isEqualToString:@"displaysAsynchronously"]) {
return @YES;
} else if ([key isEqualToString:@"opaque"]) {
return @YES;
} else {
return [super defaultValueForKey:key];
}
}
#pragma mark - Display
- (void)displayImmediately
{
// This method is a low-level bypass that avoids touching CA, including any reset of the
// needsDisplay flag, until the .contents property is set with the result.
// It is designed to be able to block the thread of any caller and fully execute the display.
ASDisplayNodeAssertMainThread();
[self display:NO];
}
- (void)_hackResetNeedsDisplay
{
ASDisplayNodeAssertMainThread();
// Don't listen to our subclasses crazy ideas about setContents by going through super
super.contents = super.contents;
}
- (void)display
{
ASDisplayNodeAssertMainThread();
[self _hackResetNeedsDisplay];
if (self.displaySuspended) {
return;
}
[self display:self.displaysAsynchronously];
}
- (void)display:(BOOL)asynchronously
{
if (CGRectIsEmpty(self.bounds)) {
_attemptedDisplayWhileZeroSized = YES;
}
[self.asyncDelegate displayAsyncLayer:self asynchronously:asynchronously];
}
- (void)cancelAsyncDisplay
{
ASDisplayNodeAssertMainThread();
[self.asyncDelegate cancelDisplayAsyncLayer:self];
}
// e.g. <MYTextNodeLayer: 0xFFFFFF; node = <MYTextNode: 0xFFFFFFE; name = "Username node for user 179">>
- (NSString *)description
{
NSMutableString *description = [[super description] mutableCopy];
ASDisplayNode *node = self.asyncdisplaykit_node;
if (node != nil) {
NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)];
[description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)];
NSUInteger insertionIndex = [description rangeOfString:@">"].location;
if (insertionIndex != NSNotFound) {
NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node];
[description insertString:nodeString atIndex:insertionIndex];
}
}
return description;
}
@end

View File

@ -0,0 +1,569 @@
//
// _ASDisplayView.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASDisplayView.h>
#import "_ASDisplayViewAccessiblity.h"
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#pragma mark - _ASDisplayViewMethodOverrides
typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides)
{
_ASDisplayViewMethodOverrideNone = 0,
_ASDisplayViewMethodOverrideCanBecomeFirstResponder = 1 << 0,
_ASDisplayViewMethodOverrideBecomeFirstResponder = 1 << 1,
_ASDisplayViewMethodOverrideCanResignFirstResponder = 1 << 2,
_ASDisplayViewMethodOverrideResignFirstResponder = 1 << 3,
_ASDisplayViewMethodOverrideIsFirstResponder = 1 << 4,
};
/**
* Returns _ASDisplayViewMethodOverrides for the given class
*
* @param c the class, required.
*
* @return _ASDisplayViewMethodOverrides.
*/
static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c)
{
ASDisplayNodeCAssertNotNil(c, @"class is required");
_ASDisplayViewMethodOverrides overrides = _ASDisplayViewMethodOverrideNone;
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canBecomeFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideCanBecomeFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(becomeFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideBecomeFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canResignFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideCanResignFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(resignFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideResignFirstResponder;
}
if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(isFirstResponder))) {
overrides |= _ASDisplayViewMethodOverrideIsFirstResponder;
}
return overrides;
}
#pragma mark - _ASDisplayView
@interface _ASDisplayView ()
// Keep the node alive while its view is active. If you create a view, add its layer to a layer hierarchy, then release
// the view, the layer retains the view to prevent a crash. This replicates this behaviour for the node abstraction.
@property (nonatomic) ASDisplayNode *keepalive_node;
@end
@implementation _ASDisplayView
{
BOOL _inHitTest;
BOOL _inPointInside;
NSArray *_accessibilityElements;
CGRect _lastAccessibilityElementsFrame;
_ASDisplayViewMethodOverrides _methodOverrides;
}
#pragma mark - Class
+ (void)initialize
{
__unused Class initializeSelf = self;
IMP staticInitialize = imp_implementationWithBlock(^(_ASDisplayView *view) {
ASDisplayNodeAssert(view.class == initializeSelf, @"View class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", view.class, initializeSelf);
view->_methodOverrides = GetASDisplayViewMethodOverrides(view.class);
});
class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@");
}
+ (Class)layerClass
{
return [_ASDisplayLayer class];
}
#pragma mark - NSObject Overrides
- (instancetype)init
{
if (!(self = [super init]))
return nil;
[self _initializeInstance];
return self;
}
- (void)_initializeInstance
{
[self _staticInitialize];
}
- (void)_staticInitialize
{
ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden");
}
// e.g. <MYPhotoNodeView: 0xFFFFFF; node = <MYPhotoNode: 0xFFFFFE>; frame = ...>
- (NSString *)description
{
NSMutableString *description = [[super description] mutableCopy];
ASDisplayNode *node = _asyncdisplaykit_node;
if (node != nil) {
NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)];
[description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)];
NSUInteger semicolon = [description rangeOfString:@";"].location;
if (semicolon != NSNotFound) {
NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node];
[description insertString:nodeString atIndex:semicolon];
}
// Remove layer description it never contains valuable info and it duplicates the node info. Noisy.
NSRange layerDescriptionRange = [description rangeOfString:@"; layer = <.*>" options:NSRegularExpressionSearch];
if (layerDescriptionRange.location != NSNotFound) {
[description replaceCharactersInRange:layerDescriptionRange withString:@""];
// Our regex will grab the closing angle bracket and I'm not clever enough to come up with a better one, so re-add it if needed.
if ([description hasSuffix:@">"] == NO) {
[description appendString:@">"];
}
}
}
return description;
}
#pragma mark - UIView Overrides
- (void)willMoveToWindow:(UIWindow *)newWindow
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
BOOL visible = (newWindow != nil);
if (visible && !node.inHierarchy) {
[node __enterHierarchy];
}
}
- (void)didMoveToWindow
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
BOOL visible = (self.window != nil);
if (!visible && node.inHierarchy) {
[node __exitHierarchy];
}
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
// Keep the node alive while the view is in a view hierarchy. This helps ensure that async-drawing views can always
// display their contents as long as they are visible somewhere, and aids in lifecycle management because the
// lifecycle of the node can be treated as the same as the lifecycle of the view (let the view hierarchy own the
// view).
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
UIView *currentSuperview = self.superview;
if (!currentSuperview && newSuperview) {
self.keepalive_node = node;
}
if (newSuperview) {
ASDisplayNode *supernode = node.supernode;
BOOL supernodeLoaded = supernode.nodeLoaded;
ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for _ASDisplayView's supernode to be layer-backed.");
BOOL needsSupernodeUpdate = NO;
if (supernode) {
if (supernodeLoaded) {
if (supernode.layerBacked) {
// See comment in -didMoveToSuperview. This case should be avoided, but is possible with app-level coding errors.
needsSupernodeUpdate = (supernode.layer != newSuperview.layer);
} else {
// If we have a supernode, compensate for users directly messing with views by hitching up to any new supernode.
needsSupernodeUpdate = (supernode.view != newSuperview);
}
} else {
needsSupernodeUpdate = YES;
}
} else {
// If we have no supernode and we are now in a view hierarchy, check to see if we can hook up to a supernode.
needsSupernodeUpdate = (newSuperview != nil);
}
if (needsSupernodeUpdate) {
// -removeFromSupernode is called by -addSubnode:, if it is needed.
// FIXME: Needs rethinking if automaticallyManagesSubnodes=YES
[newSuperview.asyncdisplaykit_node _addSubnode:node];
}
}
}
- (void)didMoveToSuperview
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
UIView *superview = self.superview;
if (superview == nil) {
// Clearing keepalive_node may cause deallocation of the node. In this case, __exitHierarchy may not have an opportunity (e.g. _node will be cleared
// by the time -didMoveToWindow occurs after this) to clear the Visible interfaceState, which we need to do before deallocation to meet an API guarantee.
if (node.inHierarchy) {
[node __exitHierarchy];
}
self.keepalive_node = nil;
}
#ifndef MINIMAL_ASDK
#if DEBUG
// This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller.
// Avoid overhead in release.
if (superview && node.viewControllerRoot) {
UIViewController *vc = [node closestViewController];
ASDisplayNodeAssert(vc != nil && [vc isKindOfClass:[ASViewController class]] && ((ASViewController*)vc).node == node, @"This node was once used as a view controller's node. You should not reuse it without its view controller.");
}
#endif
#endif
ASDisplayNode *supernode = node.supernode;
ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed.");
if (supernode) {
ASDisplayNodeAssertTrue(node.nodeLoaded);
BOOL supernodeLoaded = supernode.nodeLoaded;
BOOL needsSupernodeRemoval = NO;
if (superview) {
// If our new superview is not the same as the supernode's view, or the supernode has no view, disconnect.
if (supernodeLoaded) {
if (supernode.layerBacked) {
// As asserted at the top, this shouldn't be possible, but in production with assertions disabled it can happen.
// We try to make such code behave as well as feasible because it's not that hard of an error to make if some deep
// child node of a layer-backed node happens to be view-backed, but it is not supported and should be avoided.
needsSupernodeRemoval = (supernode.layer != superview.layer);
} else {
needsSupernodeRemoval = (supernode.view != superview);
}
} else {
needsSupernodeRemoval = YES;
}
} else {
// If supernode is loaded but our superview is nil, the user likely manually removed us, so disconnect supernode.
// The unlikely alternative: we are in __unloadNode, with shouldRasterizeSubnodes just having been turned on.
// In the latter case, we don't want to disassemble the node hierarchy because all views are intentionally being destroyed.
BOOL nodeIsRasterized = ((node.hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized);
needsSupernodeRemoval = (supernodeLoaded && !nodeIsRasterized);
}
if (needsSupernodeRemoval) {
// The node will only disconnect from its supernode, not removeFromSuperview, in this condition.
// FIXME: Needs rethinking if automaticallyManagesSubnodes=YES
[node _removeFromSupernode];
}
}
}
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index {
[super insertSubview:view atIndex:index];
#ifndef ASDK_ACCESSIBILITY_DISABLE
self.accessibilityElements = nil;
#endif
}
- (void)addSubview:(UIView *)view
{
[super addSubview:view];
#ifndef ASDK_ACCESSIBILITY_DISABLE
self.accessibilityElements = nil;
#endif
}
- (void)willRemoveSubview:(UIView *)subview
{
[super willRemoveSubview:subview];
#ifndef ASDK_ACCESSIBILITY_DISABLE
self.accessibilityElements = nil;
#endif
}
- (CGSize)sizeThatFits:(CGSize)size
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size];
}
- (void)setNeedsDisplay
{
ASDisplayNodeAssertMainThread();
// Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:.
[self.layer setNeedsDisplay];
}
- (UIViewContentMode)contentMode
{
return ASDisplayNodeUIContentModeFromCAContentsGravity(self.layer.contentsGravity);
}
- (void)setContentMode:(UIViewContentMode)contentMode
{
ASDisplayNodeAssert(contentMode != UIViewContentModeRedraw, @"Don't do this. Use needsDisplayOnBoundsChange instead.");
// Do our own mapping so as not to call super and muck up needsDisplayOnBoundsChange. If we're in a production build, fall back to resize if we see redraw
self.layer.contentsGravity = (contentMode != UIViewContentModeRedraw) ? ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode) : kCAGravityResize;
}
- (void)setBounds:(CGRect)bounds
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super setBounds:bounds];
node.threadSafeBounds = bounds;
}
- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
[super addGestureRecognizer:gestureRecognizer];
[_asyncdisplaykit_node nodeViewDidAddGestureRecognizer];
}
#pragma mark - Event Handling + UIResponder Overrides
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesBegan) {
[node touchesBegan:touches withEvent:event];
} else {
[super touchesBegan:touches withEvent:event];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesMoved) {
[node touchesMoved:touches withEvent:event];
} else {
[super touchesMoved:touches withEvent:event];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesEnded) {
[node touchesEnded:touches withEvent:event];
} else {
[super touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesCancelled) {
[node touchesCancelled:touches withEvent:event];
} else {
[super touchesCancelled:touches withEvent:event];
}
}
- (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
}
- (void)__forwardTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
}
- (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
}
- (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// REVIEW: We should optimize these types of messages by setting a boolean in the associated ASDisplayNode subclass if
// they actually override the method. Same goes for -pointInside:withEvent: below. Many UIKit classes use that
// pattern for meaningful reductions of message send overhead in hot code (especially event handling).
// Set boolean so this method can be re-entrant. If the node subclass wants to default to / make use of UIView
// hitTest:, it will call it on the view, which is _ASDisplayView. After calling into the node, any additional calls
// should use the UIView implementation of hitTest:
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (!_inHitTest) {
_inHitTest = YES;
UIView *hitView = [node hitTest:point withEvent:event];
_inHitTest = NO;
return hitView;
} else {
return [super hitTest:point withEvent:event];
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// See comments in -hitTest:withEvent: for the strategy here.
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
if (!_inPointInside) {
_inPointInside = YES;
BOOL result = [node pointInside:point withEvent:event];
_inPointInside = NO;
return result;
} else {
return [super pointInside:point withEvent:event];
}
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node gestureRecognizerShouldBegin:gestureRecognizer];
}
- (void)tintColorDidChange
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super tintColorDidChange];
[node tintColorDidChange];
}
#pragma mark UIResponder Handling
#define IMPLEMENT_RESPONDER_METHOD(__sel, __nodeMethodOverride, __viewMethodOverride) \
- (BOOL)__sel\
{\
ASDisplayNode *node = _asyncdisplaykit_node; /* Create strong reference to weak ivar. */ \
/* Check if we can call through to ASDisplayNode subclass directly */ \
if (node.methodOverrides & __nodeMethodOverride) { \
return [node __sel]; \
} else { \
/* Prevent an infinite loop in here if [super __sel] was called on a \
/ _ASDisplayView subclass */ \
if (self->_methodOverrides & __viewMethodOverride) { \
/* Call through to views superclass as we expect super was called from the
_ASDisplayView subclass and a node subclass does not overwrite __sel */ \
return [self __##__sel]; \
} else { \
/* Call through to internal node __sel method that will consider the view in responding */ \
return [node __##__sel]; \
} \
} \
}\
/* All __ prefixed methods are called from ASDisplayNode to let the view decide in what UIResponder state they \
are not overridden by a ASDisplayNode subclass */ \
- (BOOL)__##__sel \
{ \
return [super __sel]; \
} \
IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder,
ASDisplayNodeMethodOverrideCanBecomeFirstResponder,
_ASDisplayViewMethodOverrideCanBecomeFirstResponder);
IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder,
ASDisplayNodeMethodOverrideBecomeFirstResponder,
_ASDisplayViewMethodOverrideBecomeFirstResponder);
IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder,
ASDisplayNodeMethodOverrideCanResignFirstResponder,
_ASDisplayViewMethodOverrideCanResignFirstResponder);
IMPLEMENT_RESPONDER_METHOD(resignFirstResponder,
ASDisplayNodeMethodOverrideResignFirstResponder,
_ASDisplayViewMethodOverrideResignFirstResponder);
IMPLEMENT_RESPONDER_METHOD(isFirstResponder,
ASDisplayNodeMethodOverrideIsFirstResponder,
_ASDisplayViewMethodOverrideIsFirstResponder);
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
// We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:.
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]);
}
- (void)layoutMarginsDidChange
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super layoutMarginsDidChange];
[node layoutMarginsDidChange];
}
- (void)safeAreaInsetsDidChange
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super safeAreaInsetsDidChange];
[node safeAreaInsetsDidChange];
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// Ideally, we would implement -targetForAction:withSender: and simply return the node where we don't respond personally.
// Unfortunately UIResponder's default implementation of -targetForAction:withSender: doesn't follow its own documentation. It doesn't call -targetForAction:withSender: up the responder chain when -canPerformAction:withSender: fails, but instead merely calls -canPerformAction:withSender: on itself and then up the chain. rdar://20111500.
// Consequently, to forward responder-chain actions to our node, we override -canPerformAction:withSender: (used by the chain) to indicate support for responder chain-driven actions that our node supports, and then provide the node as a forwarding target here.
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return node;
}
#if TARGET_OS_TV
#pragma mark - tvOS
- (BOOL)canBecomeFocused
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node canBecomeFocused];
}
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
}
- (void)setNeedsFocusUpdate
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node setNeedsFocusUpdate];
}
- (void)updateFocusIfNeeded
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node updateFocusIfNeeded];
}
- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node shouldUpdateFocusInContext:context];
}
- (UIView *)preferredFocusedView
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return [node preferredFocusedView];
}
#endif
@end

View File

@ -0,0 +1,17 @@
//
// _ASDisplayViewAccessiblity.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/_ASDisplayView.h>
// WARNING: When dealing with accessibility elements, please use the `accessibilityElements`
// property instead of the older methods e.g. `accessibilityElementCount()`. While the older methods
// should still work as long as accessibility is enabled, this framework provides no guarantees on
// their correctness. For details, see
// https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements

View File

@ -0,0 +1,349 @@
//
// _ASDisplayViewAccessiblity.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#ifndef ASDK_ACCESSIBILITY_DISABLE
#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import "ASDisplayNodeInternal.h"
#import <queue>
NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() {
return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton;
}
#pragma mark - UIAccessibilityElement
@protocol ASAccessibilityElementPositioning
@property (nonatomic, readonly) CGRect accessibilityFrame;
@end
typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id<ASAccessibilityElementPositioning>, id<ASAccessibilityElementPositioning>);
/// Sort accessiblity elements first by y and than by x origin.
static void SortAccessibilityElements(NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
static SortAccessibilityElementsComparator comparator = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
comparator = ^NSComparisonResult(id<ASAccessibilityElementPositioning> a, id<ASAccessibilityElementPositioning> b) {
CGPoint originA = a.accessibilityFrame.origin;
CGPoint originB = b.accessibilityFrame.origin;
if (originA.y == originB.y) {
if (originA.x == originB.x) {
return NSOrderedSame;
}
return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending;
}
return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending;
};
});
[elements sortUsingComparator:comparator];
}
@interface ASAccessibilityElement : UIAccessibilityElement<ASAccessibilityElementPositioning>
@property (nonatomic) ASDisplayNode *node;
@property (nonatomic) ASDisplayNode *containerNode;
+ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode;
@end
@implementation ASAccessibilityElement
+ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode
{
ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:container];
accessibilityElement.node = node;
accessibilityElement.containerNode = containerNode;
accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier;
accessibilityElement.accessibilityLabel = node.accessibilityLabel;
accessibilityElement.accessibilityHint = node.accessibilityHint;
accessibilityElement.accessibilityValue = node.accessibilityValue;
accessibilityElement.accessibilityTraits = node.accessibilityTraits;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel;
accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint;
accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue;
}
#endif
return accessibilityElement;
}
- (CGRect)accessibilityFrame
{
CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node];
accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.accessibilityContainer);
return accessibilityFrame;
}
@end
#pragma mark - _ASDisplayView / UIAccessibilityContainer
@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction<ASAccessibilityElementPositioning>
@property (nonatomic) UIView *container;
@property (nonatomic) ASDisplayNode *node;
@property (nonatomic) ASDisplayNode *containerNode;
@end
@implementation ASAccessibilityCustomAction
- (CGRect)accessibilityFrame
{
CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node];
accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container);
return accessibilityFrame;
}
@end
/// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container
static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) {
// For every subnode that is layer backed or it's supernode has subtree rasterization enabled
// we have to create a UIAccessibilityElement as no view for this node exists
if (currentNode != containerNode && currentNode.isAccessibilityElement) {
UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode containerNode:containerNode];
[elements addObject:accessibilityElement];
}
});
}
static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, NSMutableArray *elements) {
UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container];
NSMutableArray<ASAccessibilityElement *> *labeledNodes = [[NSMutableArray alloc] init];
NSMutableArray<ASAccessibilityCustomAction *> *actions = [[NSMutableArray alloc] init];
std::queue<ASDisplayNode *> queue;
queue.push(container);
// If the container does not have an accessibility label set, or if the label is meant for custom
// actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden
// value and do not perform the aggregation.
BOOL shouldAggregateSubnodeLabels =
(container.accessibilityLabel.length == 0) ||
(container.accessibilityTraits & InteractiveAccessibilityTraitsMask());
ASDisplayNode *node = nil;
while (!queue.empty()) {
node = queue.front();
queue.pop();
if (node != container && node.isAccessibilityContainer) {
CollectAccessibilityElementsForContainer(node, view, elements);
continue;
}
if (node.accessibilityLabel.length > 0) {
if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) {
ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)];
action.node = node;
action.containerNode = node.supernode;
action.container = node.supernode.view;
[actions addObject:action];
} else if (node == container || shouldAggregateSubnodeLabels) {
// Even though not surfaced to UIKit, create a non-interactive element for purposes of building sorted aggregated label.
ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node containerNode:container];
[labeledNodes addObject:nonInteractiveElement];
}
}
for (ASDisplayNode *subnode in node.subnodes) {
queue.push(subnode);
}
}
SortAccessibilityElements(labeledNodes);
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"];
NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new];
[attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx != 0) {
[attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]];
}
[attributedLabel appendAttributedString:(NSAttributedString *)obj];
}];
accessiblityElement.accessibilityAttributedLabel = attributedLabel;
} else
#endif
{
NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"];
accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "];
}
SortAccessibilityElements(actions);
accessiblityElement.accessibilityCustomActions = actions;
[elements addObject:accessiblityElement];
}
/// Collect all accessibliity elements for a given view and view node
static void CollectAccessibilityElementsForView(UIView *view, NSMutableArray *elements)
{
ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray");
ASDisplayNode *node = view.asyncdisplaykit_node;
static Class displayListViewClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
displayListViewClass = NSClassFromString(@"Display.ListView");
});
BOOL anySubNodeIsCollection = (nil != ASDisplayNodeFindFirstNode(node,
^BOOL(ASDisplayNode *nodeToCheck) {
if (displayListViewClass != nil && [nodeToCheck isKindOfClass:displayListViewClass]) {
return true;
}
return false;
/*return ASDynamicCast(nodeToCheck, ASCollectionNode) != nil ||
ASDynamicCast(nodeToCheck, ASTableNode) != nil;*/
}));
if (node.isAccessibilityContainer && !anySubNodeIsCollection) {
CollectAccessibilityElementsForContainer(node, view, elements);
return;
}
// Handle rasterize case
if (node.rasterizesSubtree) {
CollectUIAccessibilityElementsForNode(node, node, view, elements);
return;
}
for (ASDisplayNode *subnode in node.subnodes) {
if (subnode.isAccessibilityElement) {
// An accessiblityElement can either be a UIView or a UIAccessibilityElement
if (subnode.isLayerBacked) {
// No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node
UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode containerNode:node];
[elements addObject:accessiblityElement];
} else {
// Accessiblity element is not layer backed just add the view as accessibility element
[elements addObject:subnode.view];
}
} else if (subnode.isLayerBacked) {
// Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement
CollectUIAccessibilityElementsForNode(subnode, node, view, elements);
} else if ([subnode accessibilityElementCount] > 0) {
// UIView is itself a UIAccessibilityContainer just add it
[elements addObject:subnode.view];
}
}
}
@interface _ASDisplayView () {
NSArray *_accessibilityElements;
}
@end
@implementation _ASDisplayView (UIAccessibilityContainer)
- (void)accessibilityElementDidBecomeFocused {
ASDisplayNode *viewNode = self.asyncdisplaykit_node;
if ([viewNode respondsToSelector:@selector(accessibilityElementDidBecomeFocused)]) {
[viewNode accessibilityElementDidBecomeFocused];
}
}
/*- (bool)accessibilityActivate {
ASDisplayNode *viewNode = self.asyncdisplaykit_node;
if ([viewNode respondsToSelector:@selector(accessibilityActivate)]) {
return [viewNode accessibilityActivate];
}
return false;
}*/
#pragma mark - UIAccessibility
- (void)setAccessibilityElements:(NSArray *)accessibilityElements
{
ASDisplayNodeAssertMainThread();
_accessibilityElements = nil;
}
- (NSArray *)accessibilityElements
{
ASDisplayNodeAssertMainThread();
ASDisplayNode *viewNode = self.asyncdisplaykit_node;
if (viewNode == nil) {
return @[];
}
if (true || _accessibilityElements == nil) {
_accessibilityElements = [viewNode accessibilityElements];
}
return _accessibilityElements;
}
@end
@implementation ASDisplayNode (AccessibilityInternal)
- (NSArray *)accessibilityElements
{
if (!self.isNodeLoaded) {
ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded");
return @[];
}
NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init];
CollectAccessibilityElementsForView(self.view, accessibilityElements);
SortAccessibilityElements(accessibilityElements);
return accessibilityElements;
}
@end
@implementation _ASDisplayView (UIAccessibilityAction)
- (BOOL)accessibilityActivate {
return [self.asyncdisplaykit_node accessibilityActivate];
}
- (void)accessibilityIncrement {
[self.asyncdisplaykit_node accessibilityIncrement];
}
- (void)accessibilityDecrement {
[self.asyncdisplaykit_node accessibilityDecrement];
}
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
return [self.asyncdisplaykit_node accessibilityScroll:direction];
}
- (BOOL)accessibilityPerformEscape {
return [self.asyncdisplaykit_node accessibilityPerformEscape];
}
- (BOOL)accessibilityPerformMagicTap {
return [self.asyncdisplaykit_node accessibilityPerformMagicTap];
}
@end
#endif

View File

@ -0,0 +1,41 @@
//
// _ASPendingState.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/UIView+ASConvenience.h>
/**
Private header for ASDisplayNode.mm
_ASPendingState is a proxy for a UIView that has yet to be created.
In response to its setters, it sets an internal property and a flag that indicates that that property has been set.
When you want to configure a view from this pending state information, just call -applyToView:
*/
@interface _ASPendingState : NSObject <ASDisplayNodeViewProperties, ASDisplayProperties>
// Supports all of the properties included in the ASDisplayNodeViewProperties protocol
- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)setFrameDirectly;
- (void)applyToLayer:(CALayer *)layer;
+ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer;
+ (_ASPendingState *)pendingViewStateFromView:(UIView *)view;
@property (nonatomic, readonly) BOOL hasSetNeedsLayout;
@property (nonatomic, readonly) BOOL hasSetNeedsDisplay;
@property (nonatomic, readonly) BOOL hasChanges;
- (void)clearChanges;
@end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
//
// _ASScopeTimer.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#pragma once
/**
Must compile as c++ for this to work.
Usage:
// Can be an ivar or local variable
NSTimeInterval placeToStoreTiming;
{
// some scope
AS::ScopeTimer t(placeToStoreTiming);
DoPotentiallySlowWork();
MorePotentiallySlowWork();
}
*/
namespace AS {
struct ScopeTimer {
NSTimeInterval begin;
NSTimeInterval &outT;
ScopeTimer(NSTimeInterval &outRef) : outT(outRef) {
begin = CACurrentMediaTime();
}
~ScopeTimer() {
outT = CACurrentMediaTime() - begin;
}
};
// variant where repeated calls are summed
struct SumScopeTimer {
NSTimeInterval begin;
NSTimeInterval &outT;
BOOL enable;
SumScopeTimer(NSTimeInterval &outRef, BOOL enable = YES) : outT(outRef), enable(enable) {
if (enable) {
begin = CACurrentMediaTime();
}
}
~SumScopeTimer() {
if (enable) {
outT += CACurrentMediaTime() - begin;
}
}
};
}

View File

@ -0,0 +1,104 @@
//
// _ASTransitionContext.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/_ASTransitionContext.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASLayout.h>
NSString * const ASTransitionContextFromLayoutKey = @"org.asyncdisplaykit.ASTransitionContextFromLayoutKey";
NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransitionContextToLayoutKey";
@interface _ASTransitionContext ()
@property (weak, nonatomic) id<_ASTransitionContextLayoutDelegate> layoutDelegate;
@property (weak, nonatomic) id<_ASTransitionContextCompletionDelegate> completionDelegate;
@end
@implementation _ASTransitionContext
- (instancetype)initWithAnimation:(BOOL)animated
layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate
completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate
{
self = [super init];
if (self) {
_animated = animated;
_layoutDelegate = layoutDelegate;
_completionDelegate = completionDelegate;
}
return self;
}
#pragma mark - ASContextTransitioning Protocol Implementation
- (ASLayout *)layoutForKey:(NSString *)key
{
return [_layoutDelegate transitionContext:self layoutForKey:key];
}
- (ASSizeRange)constrainedSizeForKey:(NSString *)key
{
return [_layoutDelegate transitionContext:self constrainedSizeForKey:key];
}
- (CGRect)initialFrameForNode:(ASDisplayNode *)node
{
return [[self layoutForKey:ASTransitionContextFromLayoutKey] frameForElement:node];
}
- (CGRect)finalFrameForNode:(ASDisplayNode *)node
{
return [[self layoutForKey:ASTransitionContextToLayoutKey] frameForElement:node];
}
- (NSArray<ASDisplayNode *> *)subnodesForKey:(NSString *)key
{
NSMutableArray<ASDisplayNode *> *subnodes = [[NSMutableArray alloc] init];
for (ASLayout *sublayout in [self layoutForKey:key].sublayouts) {
[subnodes addObject:(ASDisplayNode *)sublayout.layoutElement];
}
return subnodes;
}
- (NSArray<ASDisplayNode *> *)insertedSubnodes
{
return [_layoutDelegate insertedSubnodesWithTransitionContext:self];
}
- (NSArray<ASDisplayNode *> *)removedSubnodes
{
return [_layoutDelegate removedSubnodesWithTransitionContext:self];
}
- (void)completeTransition:(BOOL)didComplete
{
[_completionDelegate transitionContext:self didComplete:didComplete];
}
@end
@interface _ASAnimatedTransitionContext ()
@property (nonatomic) ASDisplayNode *node;
@property (nonatomic) CGFloat alpha;
@end
@implementation _ASAnimatedTransitionContext
+ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alpha NS_RETURNS_RETAINED
{
_ASAnimatedTransitionContext *context = [[_ASAnimatedTransitionContext alloc] init];
context.node = node;
context.alpha = alpha;
return context;
}
@end

View File

@ -281,5 +281,6 @@ open class ASButtonNode: ASControlNode {
}
func f2() {
}
}