commit 15565873c93af939ad2abb5fee8ce0285385047e Author: Nadine Salter Date: Thu Jun 26 22:32:55 2014 -0700 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..544f025472 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +DS_Store + +*.pbxuser +*.perspective +*.perspectivev3 + +*.mode1v3 +*.mode2v3 + +*.xcodeproj/xcuserdata/*.xcuserdatad + +*.xccheckout +*.xcuserdatad + +Pods + +DerivedData +build diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec new file mode 100644 index 0000000000..e8ff2359cf --- /dev/null +++ b/AsyncDisplayKit.podspec @@ -0,0 +1,42 @@ +Pod::Spec.new do |spec| + spec.name = 'AsyncDisplayKit' + spec.version = '1.0beta' + spec.license = { :type => 'BSD' } + spec.homepage = 'https://github.com/facebook/AsyncDisplayKit' + spec.authors = { 'Nadine Salter' => 'nadi@fb.com', 'Scott Goodson' => 'scottg@fb.com' } + spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.0beta' } + + # these files mustn't be compiled with ARC enabled + mrr_source_files = [ + 'AsyncDisplayKit/ASDisplayNode.mm', + 'AsyncDisplayKit/ASControlNode.m', + 'AsyncDisplayKit/ASImageNode.mm', + 'AsyncDisplayKit/Details/_ASDisplayView.mm', + 'AsyncDisplayKit/Private/_ASPendingState.m', + ] + + spec.public_header_files = [ + 'AsyncDisplayKit/*.h', + 'AsyncDisplayKit/Details/**/*.h', + 'Base/*.h' + ] + + spec.source_files = ['AsyncDisplayKit/**/*.{h,m,mm}', 'Base/*.{h,m}'] + spec.exclude_files = mrr_source_files + + spec.requires_arc = true + spec.subspec 'no-arc' do |mrr| + mrr.requires_arc = false + mrr.source_files = mrr_source_files + end + + spec.social_media_url = 'https://twitter.com/fbOpenSource' + spec.library = 'c++' + spec.xcconfig = { + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', + 'CLANG_CXX_LIBRARY' => 'libc++' + } + + spec.ios.deployment_target = '7.0' +end diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..0ff25dd8d3 --- /dev/null +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -0,0 +1,3173 @@ + + + + + archiveVersion + 1 + classes + + objectVersion + 46 + objects + + 058D09A3195D04C000B7D73C + + children + + 058D09B1195D04C000B7D73C + 058D09C5195D04C000B7D73C + 058D09AE195D04C000B7D73C + 058D09AD195D04C000B7D73C + FAD7085290B84183BD13BA1A + + isa + PBXGroup + sourceTree + <group> + + 058D09A4195D04C000B7D73C + + attributes + + LastUpgradeCheck + 0510 + ORGANIZATIONNAME + Facebook + + buildConfigurationList + 058D09A7195D04C000B7D73C + compatibilityVersion + Xcode 3.2 + developmentRegion + English + hasScannedForEncodings + 0 + isa + PBXProject + knownRegions + + en + + mainGroup + 058D09A3195D04C000B7D73C + productRefGroup + 058D09AD195D04C000B7D73C + projectDirPath + + projectReferences + + projectRoot + + targets + + 058D09AB195D04C000B7D73C + 058D09BB195D04C000B7D73C + + + 058D09A7195D04C000B7D73C + + buildConfigurations + + 058D09CD195D04C000B7D73C + 058D09CE195D04C000B7D73C + + defaultConfigurationIsVisible + 0 + defaultConfigurationName + Release + isa + XCConfigurationList + + 058D09A8195D04C000B7D73C + + buildActionMask + 2147483647 + files + + 058D0A26195D050800B7D73C + 058D0A23195D050800B7D73C + 058D0A1E195D050800B7D73C + 058D0A18195D050800B7D73C + 058D0A2C195D050800B7D73C + 058D0A15195D050800B7D73C + 058D0A1F195D050800B7D73C + 058D0A1D195D050800B7D73C + 058D0A2A195D050800B7D73C + 058D0A20195D050800B7D73C + 058D0A1A195D050800B7D73C + 058D0A28195D050800B7D73C + 058D0A21195D050800B7D73C + 058D0A25195D050800B7D73C + 058D0A14195D050800B7D73C + 058D0A1B195D050800B7D73C + 058D0A2B195D050800B7D73C + 058D0A24195D050800B7D73C + 058D0A1C195D050800B7D73C + 058D0A13195D050800B7D73C + 058D0A19195D050800B7D73C + 058D0A17195D050800B7D73C + 058D0A27195D050800B7D73C + 058D0A16195D050800B7D73C + 058D0A29195D050800B7D73C + 058D0A22195D050800B7D73C + + isa + PBXSourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09A9195D04C000B7D73C + + buildActionMask + 2147483647 + files + + 058D09B0195D04C000B7D73C + + isa + PBXFrameworksBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09AA195D04C000B7D73C + + buildActionMask + 2147483647 + dstPath + include/$(PRODUCT_NAME) + dstSubfolderSpec + 16 + files + + isa + PBXCopyFilesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09AB195D04C000B7D73C + + buildConfigurationList + 058D09CF195D04C000B7D73C + buildPhases + + 058D09A8195D04C000B7D73C + 058D09A9195D04C000B7D73C + 058D09AA195D04C000B7D73C + 058D0A46195D05C300B7D73C + + buildRules + + dependencies + + isa + PBXNativeTarget + name + AsyncDisplayKit + productName + AsyncDisplayKit + productReference + 058D09AC195D04C000B7D73C + productType + com.apple.product-type.library.static + + 058D09AC195D04C000B7D73C + + explicitFileType + archive.ar + includeInIndex + 0 + isa + PBXFileReference + path + libAsyncDisplayKit.a + sourceTree + BUILT_PRODUCTS_DIR + + 058D09AD195D04C000B7D73C + + children + + 058D09AC195D04C000B7D73C + 058D09BC195D04C000B7D73C + + isa + PBXGroup + name + Products + sourceTree + <group> + + 058D09AE195D04C000B7D73C + + children + + 058D09AF195D04C000B7D73C + 058D09BD195D04C000B7D73C + 058D09C0195D04C000B7D73C + EFA731F0396842FF8AB635EE + + isa + PBXGroup + name + Frameworks + sourceTree + <group> + + 058D09AF195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + wrapper.framework + name + Foundation.framework + path + System/Library/Frameworks/Foundation.framework + sourceTree + SDKROOT + + 058D09B0195D04C000B7D73C + + fileRef + 058D09AF195D04C000B7D73C + isa + PBXBuildFile + + 058D09B1195D04C000B7D73C + + children + + 058D09D5195D050800B7D73C + 058D09D6195D050800B7D73C + 058D09D7195D050800B7D73C + 058D09D8195D050800B7D73C + 058D09D9195D050800B7D73C + 058D09DA195D050800B7D73C + 058D09DB195D050800B7D73C + 058D09DC195D050800B7D73C + 058D09DD195D050800B7D73C + 058D09DE195D050800B7D73C + 058D09DF195D050800B7D73C + 058D09E0195D050800B7D73C + 058D09E1195D050800B7D73C + 058D0A01195D050800B7D73C + 058D09B2195D04C000B7D73C + + isa + PBXGroup + path + AsyncDisplayKit + sourceTree + <group> + + 058D09B2195D04C000B7D73C + + children + + 058D0A42195D058D00B7D73C + 058D09B3195D04C000B7D73C + + isa + PBXGroup + name + Supporting Files + sourceTree + <group> + + 058D09B3195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + AsyncDisplayKit-Prefix.pch + sourceTree + <group> + + 058D09B8195D04C000B7D73C + + buildActionMask + 2147483647 + files + + 058D0A3E195D057000B7D73C + 058D0A3D195D057000B7D73C + 058D0A3C195D057000B7D73C + 058D0A3F195D057000B7D73C + 058D0A3B195D057000B7D73C + 058D0A3A195D057000B7D73C + 058D0A39195D057000B7D73C + 058D0A41195D057000B7D73C + 058D0A40195D057000B7D73C + 058D0A38195D057000B7D73C + + isa + PBXSourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09B9195D04C000B7D73C + + buildActionMask + 2147483647 + files + + 058D09BE195D04C000B7D73C + 058D09C1195D04C000B7D73C + 058D09C4195D04C000B7D73C + 058D09BF195D04C000B7D73C + DB7121BCD50849C498C886FB + + isa + PBXFrameworksBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09BA195D04C000B7D73C + + buildActionMask + 2147483647 + files + + 058D09CA195D04C000B7D73C + + isa + PBXResourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D09BB195D04C000B7D73C + + buildConfigurationList + 058D09D2195D04C000B7D73C + buildPhases + + 2E61B6A0DB0F436A9DDBE86F + 058D09B8195D04C000B7D73C + 058D09B9195D04C000B7D73C + 058D09BA195D04C000B7D73C + 3B9D88CDF51B429C8409E4B6 + + buildRules + + dependencies + + 058D09C3195D04C000B7D73C + + isa + PBXNativeTarget + name + AsyncDisplayKitTests + productName + AsyncDisplayKitTests + productReference + 058D09BC195D04C000B7D73C + productType + com.apple.product-type.bundle.unit-test + + 058D09BC195D04C000B7D73C + + explicitFileType + wrapper.cfbundle + includeInIndex + 0 + isa + PBXFileReference + path + AsyncDisplayKitTests.xctest + sourceTree + BUILT_PRODUCTS_DIR + + 058D09BD195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + wrapper.framework + name + XCTest.framework + path + Library/Frameworks/XCTest.framework + sourceTree + DEVELOPER_DIR + + 058D09BE195D04C000B7D73C + + fileRef + 058D09BD195D04C000B7D73C + isa + PBXBuildFile + + 058D09BF195D04C000B7D73C + + fileRef + 058D09AF195D04C000B7D73C + isa + PBXBuildFile + + 058D09C0195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + wrapper.framework + name + UIKit.framework + path + Library/Frameworks/UIKit.framework + sourceTree + DEVELOPER_DIR + + 058D09C1195D04C000B7D73C + + fileRef + 058D09C0195D04C000B7D73C + isa + PBXBuildFile + + 058D09C2195D04C000B7D73C + + containerPortal + 058D09A4195D04C000B7D73C + isa + PBXContainerItemProxy + proxyType + 1 + remoteGlobalIDString + 058D09AB195D04C000B7D73C + remoteInfo + AsyncDisplayKit + + 058D09C3195D04C000B7D73C + + isa + PBXTargetDependency + target + 058D09AB195D04C000B7D73C + targetProxy + 058D09C2195D04C000B7D73C + + 058D09C4195D04C000B7D73C + + fileRef + 058D09AC195D04C000B7D73C + isa + PBXBuildFile + + 058D09C5195D04C000B7D73C + + children + + 058D0A2D195D057000B7D73C + 058D0A2E195D057000B7D73C + 058D0A2F195D057000B7D73C + 058D0A30195D057000B7D73C + 058D0A31195D057000B7D73C + 058D0A32195D057000B7D73C + 058D0A33195D057000B7D73C + 058D0A34195D057000B7D73C + 058D0A35195D057000B7D73C + 058D0A36195D057000B7D73C + 058D0A37195D057000B7D73C + 058D09C6195D04C000B7D73C + + isa + PBXGroup + path + AsyncDisplayKitTests + sourceTree + <group> + + 058D09C6195D04C000B7D73C + + children + + 058D09C7195D04C000B7D73C + 058D09C8195D04C000B7D73C + + isa + PBXGroup + name + Supporting Files + sourceTree + <group> + + 058D09C7195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + text.plist.xml + path + AsyncDisplayKitTests-Info.plist + sourceTree + <group> + + 058D09C8195D04C000B7D73C + + children + + 058D09C9195D04C000B7D73C + + isa + PBXVariantGroup + name + InfoPlist.strings + sourceTree + <group> + + 058D09C9195D04C000B7D73C + + isa + PBXFileReference + lastKnownFileType + text.plist.strings + name + en + path + en.lproj/InfoPlist.strings + sourceTree + <group> + + 058D09CA195D04C000B7D73C + + fileRef + 058D09C8195D04C000B7D73C + isa + PBXBuildFile + + 058D09CD195D04C000B7D73C + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + COPY_PHASE_STRIP + NO + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_DYNAMIC_NO_PIC + NO + GCC_OPTIMIZATION_LEVEL + 0 + GCC_PREPROCESSOR_DEFINITIONS + + DEBUG=1 + $(inherited) + + GCC_SYMBOLS_PRIVATE_EXTERN + NO + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 7.1 + ONLY_ACTIVE_ARCH + YES + SDKROOT + iphoneos + + isa + XCBuildConfiguration + name + Debug + + 058D09CE195D04C000B7D73C + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + COPY_PHASE_STRIP + YES + ENABLE_NS_ASSERTIONS + NO + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 7.1 + SDKROOT + iphoneos + VALIDATE_PRODUCT + YES + + isa + XCBuildConfiguration + name + Release + + 058D09CF195D04C000B7D73C + + buildConfigurations + + 058D09D0195D04C000B7D73C + 058D09D1195D04C000B7D73C + + defaultConfigurationIsVisible + 0 + isa + XCConfigurationList + + 058D09D0195D04C000B7D73C + + buildSettings + + DSTROOT + /tmp/AsyncDisplayKit.dst + GCC_PRECOMPILE_PREFIX_HEADER + YES + GCC_PREFIX_HEADER + AsyncDisplayKit/AsyncDisplayKit-Prefix.pch + OTHER_LDFLAGS + -ObjC + PRODUCT_NAME + $(TARGET_NAME) + SKIP_INSTALL + YES + + isa + XCBuildConfiguration + name + Debug + + 058D09D1195D04C000B7D73C + + buildSettings + + DSTROOT + /tmp/AsyncDisplayKit.dst + GCC_PRECOMPILE_PREFIX_HEADER + YES + GCC_PREFIX_HEADER + AsyncDisplayKit/AsyncDisplayKit-Prefix.pch + OTHER_LDFLAGS + -ObjC + PRODUCT_NAME + $(TARGET_NAME) + SKIP_INSTALL + YES + + isa + XCBuildConfiguration + name + Release + + 058D09D2195D04C000B7D73C + + buildConfigurations + + 058D09D3195D04C000B7D73C + 058D09D4195D04C000B7D73C + + defaultConfigurationIsVisible + 0 + isa + XCConfigurationList + + 058D09D3195D04C000B7D73C + + baseConfigurationReference + FAD7085290B84183BD13BA1A + buildSettings + + FRAMEWORK_SEARCH_PATHS + + $(SDKROOT)/Developer/Library/Frameworks + $(inherited) + $(DEVELOPER_FRAMEWORKS_DIR) + + GCC_PRECOMPILE_PREFIX_HEADER + YES + GCC_PREFIX_HEADER + AsyncDisplayKit/AsyncDisplayKit-Prefix.pch + GCC_PREPROCESSOR_DEFINITIONS + + DEBUG=1 + $(inherited) + + INFOPLIST_FILE + AsyncDisplayKitTests/AsyncDisplayKitTests-Info.plist + PRODUCT_NAME + $(TARGET_NAME) + WRAPPER_EXTENSION + xctest + + isa + XCBuildConfiguration + name + Debug + + 058D09D4195D04C000B7D73C + + baseConfigurationReference + FAD7085290B84183BD13BA1A + buildSettings + + FRAMEWORK_SEARCH_PATHS + + $(SDKROOT)/Developer/Library/Frameworks + $(inherited) + $(DEVELOPER_FRAMEWORKS_DIR) + + GCC_PRECOMPILE_PREFIX_HEADER + YES + GCC_PREFIX_HEADER + AsyncDisplayKit/AsyncDisplayKit-Prefix.pch + INFOPLIST_FILE + AsyncDisplayKitTests/AsyncDisplayKitTests-Info.plist + PRODUCT_NAME + $(TARGET_NAME) + WRAPPER_EXTENSION + xctest + + isa + XCBuildConfiguration + name + Release + + 058D09D5195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASControlNode.h + sourceTree + <group> + + 058D09D6195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASControlNode.m + sourceTree + <group> + + 058D09D7195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASControlNode+Subclasses.h + sourceTree + <group> + + 058D09D8195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNode.h + sourceTree + <group> + + 058D09D9195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASDisplayNode.mm + sourceTree + <group> + + 058D09DA195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNode+Subclasses.h + sourceTree + <group> + + 058D09DB195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNodeExtras.h + sourceTree + <group> + + 058D09DC195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASDisplayNodeExtras.mm + sourceTree + <group> + + 058D09DD195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASImageNode.h + sourceTree + <group> + + 058D09DE195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASImageNode.mm + sourceTree + <group> + + 058D09DF195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNode.h + sourceTree + <group> + + 058D09E0195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASTextNode.mm + sourceTree + <group> + + 058D09E1195D050800B7D73C + + children + + 058D09E2195D050800B7D73C + 058D09E3195D050800B7D73C + 058D09E4195D050800B7D73C + 058D09E5195D050800B7D73C + 058D09E6195D050800B7D73C + 058D09E7195D050800B7D73C + 058D09E8195D050800B7D73C + 058D09E9195D050800B7D73C + 058D09EA195D050800B7D73C + 058D09EB195D050800B7D73C + 058D09EC195D050800B7D73C + 058D09ED195D050800B7D73C + 058D09EE195D050800B7D73C + 058D09EF195D050800B7D73C + 058D09F0195D050800B7D73C + 058D09F1195D050800B7D73C + 058D09F2195D050800B7D73C + 058D09F3195D050800B7D73C + 058D09F4195D050800B7D73C + 058D09F5195D050800B7D73C + 058D09F6195D050800B7D73C + 058D09F7195D050800B7D73C + 058D09FF195D050800B7D73C + 058D0A00195D050800B7D73C + + isa + PBXGroup + path + Details + sourceTree + <group> + + 058D09E2195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASDisplayLayer.h + sourceTree + <group> + + 058D09E3195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + _ASDisplayLayer.mm + sourceTree + <group> + + 058D09E4195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASDisplayView.h + sourceTree + <group> + + 058D09E5195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + _ASDisplayView.mm + sourceTree + <group> + + 058D09E6195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASHighlightOverlayLayer.h + sourceTree + <group> + + 058D09E7195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASHighlightOverlayLayer.m + sourceTree + <group> + + 058D09E8195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASMutableAttributedStringBuilder.h + sourceTree + <group> + + 058D09E9195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASMutableAttributedStringBuilder.m + sourceTree + <group> + + 058D09EA195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeCoreTextAdditions.h + sourceTree + <group> + + 058D09EB195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeCoreTextAdditions.m + sourceTree + <group> + + 058D09EC195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeRenderer.h + sourceTree + <group> + + 058D09ED195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASTextNodeRenderer.mm + sourceTree + <group> + + 058D09EE195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeShadower.h + sourceTree + <group> + + 058D09EF195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeShadower.m + sourceTree + <group> + + 058D09F0195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeTextKitHelpers.h + sourceTree + <group> + + 058D09F1195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASTextNodeTextKitHelpers.mm + sourceTree + <group> + + 058D09F2195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeTypes.h + sourceTree + <group> + + 058D09F3195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASTextNodeWordKerner.h + sourceTree + <group> + + 058D09F4195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeWordKerner.m + sourceTree + <group> + + 058D09F5195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + NSMutableAttributedString+TextKitAdditions.h + sourceTree + <group> + + 058D09F6195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + NSMutableAttributedString+TextKitAdditions.m + sourceTree + <group> + + 058D09F7195D050800B7D73C + + children + + 058D09F8195D050800B7D73C + 058D09F9195D050800B7D73C + 058D09FA195D050800B7D73C + 058D09FB195D050800B7D73C + 058D09FC195D050800B7D73C + 058D09FD195D050800B7D73C + 058D09FE195D050800B7D73C + + isa + PBXGroup + path + Transactions + sourceTree + <group> + + 058D09F8195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASAsyncTransaction.h + sourceTree + <group> + + 058D09F9195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + _ASAsyncTransaction.m + sourceTree + <group> + + 058D09FA195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASAsyncTransactionContainer+Private.h + sourceTree + <group> + + 058D09FB195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASAsyncTransactionContainer.h + sourceTree + <group> + + 058D09FC195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + _ASAsyncTransactionContainer.m + sourceTree + <group> + + 058D09FD195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASAsyncTransactionGroup.h + sourceTree + <group> + + 058D09FE195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + _ASAsyncTransactionGroup.m + sourceTree + <group> + + 058D09FF195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + UIView+ASConvenience.h + sourceTree + <group> + + 058D0A00195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + UIView+ASConvenience.m + sourceTree + <group> + + 058D0A01195D050800B7D73C + + children + + 058D0A02195D050800B7D73C + 058D0A03195D050800B7D73C + 058D0A04195D050800B7D73C + 058D0A05195D050800B7D73C + 058D0A06195D050800B7D73C + 058D0A07195D050800B7D73C + 058D0A08195D050800B7D73C + 058D0A09195D050800B7D73C + 058D0A0A195D050800B7D73C + 058D0A0B195D050800B7D73C + 058D0A0C195D050800B7D73C + 058D0A0D195D050800B7D73C + 058D0A0E195D050800B7D73C + 058D0A0F195D050800B7D73C + 058D0A10195D050800B7D73C + 058D0A11195D050800B7D73C + 058D0A12195D050800B7D73C + + isa + PBXGroup + path + Private + sourceTree + <group> + + 058D0A02195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _AS-objc-internal.h + sourceTree + <group> + + 058D0A03195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASCoreAnimationExtras.h + sourceTree + <group> + + 058D0A04195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + _ASCoreAnimationExtras.mm + sourceTree + <group> + + 058D0A05195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASPendingState.h + sourceTree + <group> + + 058D0A06195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + _ASPendingState.m + sourceTree + <group> + + 058D0A07195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + _ASScopeTimer.h + sourceTree + <group> + + 058D0A08195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASDisplayNode+AsyncDisplay.mm + sourceTree + <group> + + 058D0A09195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNode+DebugTiming.h + sourceTree + <group> + + 058D0A0A195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASDisplayNode+DebugTiming.mm + sourceTree + <group> + + 058D0A0B195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASDisplayNode+UIViewBridge.mm + sourceTree + <group> + + 058D0A0C195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNodeInternal.h + sourceTree + <group> + + 058D0A0D195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASImageNode+CGExtras.h + sourceTree + <group> + + 058D0A0E195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASImageNode+CGExtras.m + sourceTree + <group> + + 058D0A0F195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASImageProtocols.h + sourceTree + <group> + + 058D0A10195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASSentinel.h + sourceTree + <group> + + 058D0A11195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASSentinel.m + sourceTree + <group> + + 058D0A12195D050800B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASThread.h + sourceTree + <group> + + 058D0A13195D050800B7D73C + + fileRef + 058D09D6195D050800B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A14195D050800B7D73C + + fileRef + 058D09D9195D050800B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A15195D050800B7D73C + + fileRef + 058D09DC195D050800B7D73C + isa + PBXBuildFile + + 058D0A16195D050800B7D73C + + fileRef + 058D09DE195D050800B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A17195D050800B7D73C + + fileRef + 058D09E0195D050800B7D73C + isa + PBXBuildFile + + 058D0A18195D050800B7D73C + + fileRef + 058D09E3195D050800B7D73C + isa + PBXBuildFile + + 058D0A19195D050800B7D73C + + fileRef + 058D09E5195D050800B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A1A195D050800B7D73C + + fileRef + 058D09E7195D050800B7D73C + isa + PBXBuildFile + + 058D0A1B195D050800B7D73C + + fileRef + 058D09E9195D050800B7D73C + isa + PBXBuildFile + + 058D0A1C195D050800B7D73C + + fileRef + 058D09EB195D050800B7D73C + isa + PBXBuildFile + + 058D0A1D195D050800B7D73C + + fileRef + 058D09ED195D050800B7D73C + isa + PBXBuildFile + + 058D0A1E195D050800B7D73C + + fileRef + 058D09EF195D050800B7D73C + isa + PBXBuildFile + + 058D0A1F195D050800B7D73C + + fileRef + 058D09F1195D050800B7D73C + isa + PBXBuildFile + + 058D0A20195D050800B7D73C + + fileRef + 058D09F4195D050800B7D73C + isa + PBXBuildFile + + 058D0A21195D050800B7D73C + + fileRef + 058D09F6195D050800B7D73C + isa + PBXBuildFile + + 058D0A22195D050800B7D73C + + fileRef + 058D09F9195D050800B7D73C + isa + PBXBuildFile + + 058D0A23195D050800B7D73C + + fileRef + 058D09FC195D050800B7D73C + isa + PBXBuildFile + + 058D0A24195D050800B7D73C + + fileRef + 058D09FE195D050800B7D73C + isa + PBXBuildFile + + 058D0A25195D050800B7D73C + + fileRef + 058D0A00195D050800B7D73C + isa + PBXBuildFile + + 058D0A26195D050800B7D73C + + fileRef + 058D0A04195D050800B7D73C + isa + PBXBuildFile + + 058D0A27195D050800B7D73C + + fileRef + 058D0A06195D050800B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A28195D050800B7D73C + + fileRef + 058D0A08195D050800B7D73C + isa + PBXBuildFile + + 058D0A29195D050800B7D73C + + fileRef + 058D0A0A195D050800B7D73C + isa + PBXBuildFile + + 058D0A2A195D050800B7D73C + + fileRef + 058D0A0B195D050800B7D73C + isa + PBXBuildFile + + 058D0A2B195D050800B7D73C + + fileRef + 058D0A0E195D050800B7D73C + isa + PBXBuildFile + + 058D0A2C195D050800B7D73C + + fileRef + 058D0A11195D050800B7D73C + isa + PBXBuildFile + + 058D0A2D195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASDisplayLayerTests.m + sourceTree + <group> + + 058D0A2E195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASDisplayNodeAppearanceTests.m + sourceTree + <group> + + 058D0A2F195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASDisplayNodeTests.m + sourceTree + <group> + + 058D0A30195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNodeTestsHelper.h + sourceTree + <group> + + 058D0A31195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASDisplayNodeTestsHelper.m + sourceTree + <group> + + 058D0A32195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASMutableAttributedStringBuilderTests.m + sourceTree + <group> + + 058D0A33195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeCoreTextAdditionsTests.m + sourceTree + <group> + + 058D0A34195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeRendererTests.m + sourceTree + <group> + + 058D0A35195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeShadowerTests.m + sourceTree + <group> + + 058D0A36195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.objc + path + ASTextNodeTests.m + sourceTree + <group> + + 058D0A37195D057000B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.cpp.objcpp + path + ASTextNodeWordKernerTests.mm + sourceTree + <group> + + 058D0A38195D057000B7D73C + + fileRef + 058D0A2D195D057000B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A39195D057000B7D73C + + fileRef + 058D0A2E195D057000B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A3A195D057000B7D73C + + fileRef + 058D0A2F195D057000B7D73C + isa + PBXBuildFile + settings + + COMPILER_FLAGS + -fno-objc-arc + + + 058D0A3B195D057000B7D73C + + fileRef + 058D0A31195D057000B7D73C + isa + PBXBuildFile + + 058D0A3C195D057000B7D73C + + fileRef + 058D0A32195D057000B7D73C + isa + PBXBuildFile + + 058D0A3D195D057000B7D73C + + fileRef + 058D0A33195D057000B7D73C + isa + PBXBuildFile + + 058D0A3E195D057000B7D73C + + fileRef + 058D0A34195D057000B7D73C + isa + PBXBuildFile + + 058D0A3F195D057000B7D73C + + fileRef + 058D0A35195D057000B7D73C + isa + PBXBuildFile + + 058D0A40195D057000B7D73C + + fileRef + 058D0A36195D057000B7D73C + isa + PBXBuildFile + + 058D0A41195D057000B7D73C + + fileRef + 058D0A37195D057000B7D73C + isa + PBXBuildFile + + 058D0A42195D058D00B7D73C + + children + + 058D0A43195D058D00B7D73C + 058D0A44195D058D00B7D73C + 058D0A45195D058D00B7D73C + + isa + PBXGroup + path + Base + sourceTree + SOURCE_ROOT + + 058D0A43195D058D00B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASAssert.h + sourceTree + <group> + + 058D0A44195D058D00B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASBaseDefines.h + sourceTree + <group> + + 058D0A45195D058D00B7D73C + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.c.h + path + ASDisplayNodeExtraIvars.h + sourceTree + <group> + + 058D0A46195D05C300B7D73C + + buildActionMask + 2147483647 + files + + 058D0A47195D05CB00B7D73C + 058D0A48195D05CB00B7D73C + 058D0A49195D05CB00B7D73C + 058D0A4A195D05CB00B7D73C + 058D0A4B195D05CB00B7D73C + 058D0A4C195D05CB00B7D73C + 058D0A4D195D05CB00B7D73C + 058D0A4E195D05CB00B7D73C + 058D0A4F195D05CB00B7D73C + 058D0A50195D05CB00B7D73C + 058D0A51195D05CB00B7D73C + 058D0A52195D05CB00B7D73C + 058D0A53195D05DC00B7D73C + 058D0A54195D05DC00B7D73C + 058D0A55195D05DC00B7D73C + 058D0A56195D05DC00B7D73C + 058D0A57195D05DC00B7D73C + 058D0A58195D05DC00B7D73C + 058D0A59195D05DC00B7D73C + 058D0A5A195D05DC00B7D73C + 058D0A5B195D05DC00B7D73C + 058D0A5C195D05DC00B7D73C + 058D0A5D195D05DC00B7D73C + 058D0A5E195D05DC00B7D73C + 058D0A5F195D05DC00B7D73C + 058D0A60195D05DC00B7D73C + 058D0A61195D05DC00B7D73C + 058D0A62195D05DC00B7D73C + 058D0A63195D05DC00B7D73C + 058D0A64195D05DC00B7D73C + 058D0A65195D05DC00B7D73C + 058D0A66195D05DC00B7D73C + 058D0A67195D05DC00B7D73C + 058D0A68195D05EC00B7D73C + 058D0A69195D05EC00B7D73C + 058D0A6A195D05EC00B7D73C + 058D0A6B195D05EC00B7D73C + 058D0A6C195D05EC00B7D73C + 058D0A6D195D05EC00B7D73C + 058D0A6E195D05EC00B7D73C + 058D0A6F195D05EC00B7D73C + 058D0A70195D05EC00B7D73C + 058D0A82195D060300B7D73C + 058D0A83195D060300B7D73C + 058D0A84195D060300B7D73C + 058D0A71195D05F800B7D73C + 058D0A72195D05F800B7D73C + 058D0A73195D05F800B7D73C + 058D0A74195D05F800B7D73C + 058D0A75195D05F800B7D73C + 058D0A76195D05F900B7D73C + 058D0A77195D05F900B7D73C + 058D0A78195D05F900B7D73C + 058D0A79195D05F900B7D73C + 058D0A7A195D05F900B7D73C + 058D0A7B195D05F900B7D73C + 058D0A7C195D05F900B7D73C + 058D0A7D195D05F900B7D73C + 058D0A7E195D05F900B7D73C + 058D0A7F195D05F900B7D73C + 058D0A80195D05F900B7D73C + 058D0A81195D05F900B7D73C + + isa + PBXHeadersBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 058D0A47195D05CB00B7D73C + + fileRef + 058D09D5195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A48195D05CB00B7D73C + + fileRef + 058D09D6195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A49195D05CB00B7D73C + + fileRef + 058D09D7195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4A195D05CB00B7D73C + + fileRef + 058D09D8195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4B195D05CB00B7D73C + + fileRef + 058D09D9195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4C195D05CB00B7D73C + + fileRef + 058D09DA195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4D195D05CB00B7D73C + + fileRef + 058D09DB195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4E195D05CB00B7D73C + + fileRef + 058D09DC195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A4F195D05CB00B7D73C + + fileRef + 058D09DD195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A50195D05CB00B7D73C + + fileRef + 058D09DE195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A51195D05CB00B7D73C + + fileRef + 058D09DF195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A52195D05CB00B7D73C + + fileRef + 058D09E0195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A53195D05DC00B7D73C + + fileRef + 058D09E2195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A54195D05DC00B7D73C + + fileRef + 058D09E3195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A55195D05DC00B7D73C + + fileRef + 058D09E4195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A56195D05DC00B7D73C + + fileRef + 058D09E5195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A57195D05DC00B7D73C + + fileRef + 058D09E6195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A58195D05DC00B7D73C + + fileRef + 058D09E7195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A59195D05DC00B7D73C + + fileRef + 058D09E8195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5A195D05DC00B7D73C + + fileRef + 058D09E9195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5B195D05DC00B7D73C + + fileRef + 058D09EA195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5C195D05DC00B7D73C + + fileRef + 058D09EB195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5D195D05DC00B7D73C + + fileRef + 058D09EC195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5E195D05DC00B7D73C + + fileRef + 058D09ED195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A5F195D05DC00B7D73C + + fileRef + 058D09EE195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A60195D05DC00B7D73C + + fileRef + 058D09EF195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A61195D05DC00B7D73C + + fileRef + 058D09F0195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A62195D05DC00B7D73C + + fileRef + 058D09F1195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A63195D05DC00B7D73C + + fileRef + 058D09F2195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A64195D05DC00B7D73C + + fileRef + 058D09F3195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A65195D05DC00B7D73C + + fileRef + 058D09F4195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A66195D05DC00B7D73C + + fileRef + 058D09F5195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A67195D05DC00B7D73C + + fileRef + 058D09F6195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A68195D05EC00B7D73C + + fileRef + 058D09F8195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A69195D05EC00B7D73C + + fileRef + 058D09F9195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6A195D05EC00B7D73C + + fileRef + 058D09FA195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6B195D05EC00B7D73C + + fileRef + 058D09FB195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6C195D05EC00B7D73C + + fileRef + 058D09FC195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6D195D05EC00B7D73C + + fileRef + 058D09FD195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6E195D05EC00B7D73C + + fileRef + 058D09FE195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A6F195D05EC00B7D73C + + fileRef + 058D09FF195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A70195D05EC00B7D73C + + fileRef + 058D0A00195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A71195D05F800B7D73C + + fileRef + 058D0A02195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A72195D05F800B7D73C + + fileRef + 058D0A03195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A73195D05F800B7D73C + + fileRef + 058D0A04195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A74195D05F800B7D73C + + fileRef + 058D0A05195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A75195D05F800B7D73C + + fileRef + 058D0A06195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A76195D05F900B7D73C + + fileRef + 058D0A07195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A77195D05F900B7D73C + + fileRef + 058D0A08195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A78195D05F900B7D73C + + fileRef + 058D0A09195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A79195D05F900B7D73C + + fileRef + 058D0A0A195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7A195D05F900B7D73C + + fileRef + 058D0A0B195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7B195D05F900B7D73C + + fileRef + 058D0A0C195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7C195D05F900B7D73C + + fileRef + 058D0A0D195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7D195D05F900B7D73C + + fileRef + 058D0A0E195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7E195D05F900B7D73C + + fileRef + 058D0A0F195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A7F195D05F900B7D73C + + fileRef + 058D0A10195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A80195D05F900B7D73C + + fileRef + 058D0A11195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A81195D05F900B7D73C + + fileRef + 058D0A12195D050800B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Private + + + + 058D0A82195D060300B7D73C + + fileRef + 058D0A43195D058D00B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A83195D060300B7D73C + + fileRef + 058D0A44195D058D00B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 058D0A84195D060300B7D73C + + fileRef + 058D0A45195D058D00B7D73C + isa + PBXBuildFile + settings + + ATTRIBUTES + + Public + + + + 2E61B6A0DB0F436A9DDBE86F + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + Check Pods Manifest.lock + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null +if [[ $? != 0 ]] ; then + cat << EOM +error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. +EOM + exit 1 +fi + + showEnvVarsInLog + 0 + + 3B9D88CDF51B429C8409E4B6 + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + Copy Pods Resources + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + "${SRCROOT}/Pods/Pods-AsyncDisplayKitTests-resources.sh" + + showEnvVarsInLog + 0 + + DB7121BCD50849C498C886FB + + fileRef + EFA731F0396842FF8AB635EE + isa + PBXBuildFile + + EFA731F0396842FF8AB635EE + + explicitFileType + archive.ar + includeInIndex + 0 + isa + PBXFileReference + path + libPods-AsyncDisplayKitTests.a + sourceTree + BUILT_PRODUCTS_DIR + + FAD7085290B84183BD13BA1A + + includeInIndex + 1 + isa + PBXFileReference + lastKnownFileType + text.xcconfig + name + Pods-AsyncDisplayKitTests.xcconfig + path + Pods/Pods-AsyncDisplayKitTests.xcconfig + sourceTree + <group> + + + rootObject + 058D09A4195D04C000B7D73C + + diff --git a/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata b/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..1ba280a045 --- /dev/null +++ b/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/AsyncDisplayKit/ASControlNode+Subclasses.h b/AsyncDisplayKit/ASControlNode+Subclasses.h new file mode 100644 index 0000000000..eb8819e35b --- /dev/null +++ b/AsyncDisplayKit/ASControlNode+Subclasses.h @@ -0,0 +1,55 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASControlNode.h" + +@interface ASControlNode (ForSubclassesEyesOnly) + +/** + @abstract Sends action messages for the given control events. + @param controlEvents A bitmask whose set flags specify the control events for which action messages are sent. See "Control Events" in ASControlNode.h for bitmask constants. + @param event An event object encapsulating the information specific to the user event. + @disucssion ASControlNode implements this method to send all action messages associated with controlEvents. The list of targets is constructed from prior invocations of addTarget:action:forControlEvents:. + */ +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)touchEvent; + +/** + @abstract Sent to the control when tracking begins. + @param touch The touch on the receiving control. + @param touchEvent An event object encapsulating the information specific to the user event. + @result YES if the receiver should respond continuously (respond when touch is dragged); NO otherwise. + */ +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent; + +/** + @abstract Sent continuously to the control as it tracks a touch within the control's bounds. + @param touch The touch on the receiving control. + @param touchevent An event object encapsulating the information specific to the user event. + @result YES if touch tracking should continue; NO otherwise. + */ +- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent; + +/** + @abstract Sent to the control when tracking should be cancelled. + @param touchEvent An event object encapsulating the information specific to the user event. This parameter may be nil, indicating that the cancelation was caused by something other than an event, such as the display node being removed from its supernode. + */ +- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent; + +/** + @abstract Sent to the control when the last touch completely ends, telling it to stop tracking. + @param touch The touch that ended. + @param touchEvent An event object encapsulating the information specific to the user event. + */ +- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent; + +/** + @abstract Settable version of highlighted property. + */ +@property (nonatomic, readwrite, assign, getter=isHighlighted) BOOL highlighted; + +@end diff --git a/AsyncDisplayKit/ASControlNode.h b/AsyncDisplayKit/ASControlNode.h new file mode 100644 index 0000000000..28a814c553 --- /dev/null +++ b/AsyncDisplayKit/ASControlNode.h @@ -0,0 +1,101 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDisplayNode.h" + +/** + @abstract Kinds of events possible for control nodes. + @discussion These events are identical to their UIControl counterparts. + */ +enum _ASControlNodeEvent +{ + ASControlNodeEventTouchDown = 1 << 0, + ASControlNodeEventTouchDownRepeat = 1 << 1, + ASControlNodeEventTouchDragInside = 1 << 2, + ASControlNodeEventTouchDragOutside = 1 << 3, + ASControlNodeEventTouchUpInside = 1 << 4, + ASControlNodeEventTouchUpOutside = 1 << 5, + ASControlNodeEventTouchCancel = 1 << 6, + + ASControlNodeEventAllEvents = 0xFFFFFFFF +}; +typedef NSUInteger ASControlNodeEvent; + +/** + @abstract ASControlNode is the base class for control nodes (such as buttons), or nodes that track touches to invoke targets with action messages. + @discussion ASControlNode cannot be used directly. It instead defines the common interface and behavior structure for all its subclasses. Subclasses should import "ASControlNode+Subclasses.h" for information on methods intended to be overriden. + */ +@interface ASControlNode : ASDisplayNode + +#pragma mark - Control State + +/** + @abstract Indicates whether or not the receiver is enabled. + @discussion Specify YES to make the control enabled; otherwise, specify NO to make it disabled. The default value is YES. If the enabled state is NO, the control ignores touch events and subclasses may draw differently. + */ +@property (nonatomic, readwrite, assign, getter=isEnabled) BOOL enabled; + +/** + @abstract Indicates whether or not the receiver is highlighted. + @discussion This is set automatically when the there is a touch inside the control and removed on exit or touch up. This is different from touchInside in that it includes an area around the control, rather than just for touches inside the control. + */ +@property (nonatomic, readonly, assign, getter=isHighlighted) BOOL highlighted; + +#pragma mark - Tracking Touches +/** + @abstract Indicates whether or not the receiver is currently tracking touches related to an event. + @discussion YES if the receiver is tracking touches; NO otherwise. + */ +@property (nonatomic, readonly, assign, getter=isTracking) BOOL tracking; + +/** + @abstract Indicates whether or not a touch is inside the bounds of the receiver. + @discussion YES if a touch is inside the receiver's bounds; NO otherwise. + */ +@property (nonatomic, readonly, assign, getter=isTouchInside) BOOL touchInside; + +#pragma mark - Action Messages +/** + @abstract Adds a target-action pair for a particular event (or events). + @param target The object to which the action message is sent. If this is nil, the responder chain is searched for an object willing to respond to the action message. target is not retained. + @param action A selector identifying an action message. May optionally include the sender and the event as parameters, in that order. May not be NULL. + @param controlEvents A bitmask specifying the control events for which the action message is sent. May not be 0. See "Control Events" for bitmask constants. + @discussion You may call this method multiple times, and you may specify multiple target-action pairs for a particular event. + */ +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEvents; + +/** + @abstract Returns the actions that are associated with a target and a particular control event. + @param target The target object. May not be nil. + @param controlEvent A single constant of type ASControlNodeEvent that specifies a particular user action on the control; for a list of these constants, see "Control Events". May not be 0 or ASControlNodeEventAllEvents. + @result An array of selector names as NSString objects, or nil if there are no action selectors associated with controlEvent. + */ +- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent; + +/** + @abstract Returns all target objects associated with the receiver. + @result A set of all targets for the receiver. The set may include NSNull to indicate at least one nil target (meaning, the responder chain is searched for a target.) + */ +- (NSSet *)allTargets; + +/** + @abstract Removes a target-action pair for a particular event. + @param target The target object. Pass nil to remove all targets paired with action and the specified control events. + @param action A selector identifying an action message. Pass NULL to remove all action messages paired with target. + @param controlEvents A bitmask specifying the control events associated with target and action. See "Control Events" for bitmask constants. May not be 0. + */ +- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEvents; + +/** + @abstract Sends the actions for the control events for a particular event. + @param controlEvents A bitmask specifying the control events for which to send actions. See "Control Events" for bitmask constants. May not be 0. + @param event The event which triggered these control actions. May be nil. + */ +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event; + +@end diff --git a/AsyncDisplayKit/ASControlNode.m b/AsyncDisplayKit/ASControlNode.m new file mode 100644 index 0000000000..9d28232dc9 --- /dev/null +++ b/AsyncDisplayKit/ASControlNode.m @@ -0,0 +1,438 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASControlNode.h" +#import "ASControlNode+Subclasses.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 kASControlNodeTargetDispatchTableInitialCapacity 2 +#define kASControlNodeActionDispatchTableInitialCapacity 4 + +@interface ASControlNode () +{ +@private + // Control Attributes + BOOL _enabled; + BOOL _highlighted; + + // Tracking + BOOL _tracking; + BOOL _touchInside; + + // Target Messages. + /* + The table structure is as follows: + + { + AnEvent -> { + target1 -> (action1, ...) + target2 -> (action1, ...) + ... + } + ... + } + */ + __block NSMutableDictionary *_controlEventDispatchTable; +} + +// Read-write overrides. +@property (nonatomic, readwrite, assign, getter=isHighlighted) BOOL highlighted; +@property (nonatomic, readwrite, assign, getter=isTracking) BOOL tracking; +@property (nonatomic, readwrite, assign, getter=isTouchInside) BOOL touchInside; + +/** + @abstract Indicates whether the receiver is interested in receiving touches. + */ +- (BOOL)_isInterestedInTouches; + +/** + @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 _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent); + +/** + @abstract Returns a key to be used inside the dictionaries within _controlEventDispatchTable that identifies the target. + @param target A target. May safely be nil. + @result A key for use in in the dictionaries within _controlEventDispatchTable. + */ +id _ASControlNodeTargetKeyForTarget(id target); + +/** + @abstract Returns the target for invocation from a given targetKey. + @param targetKey A target key created with _ASControlNodeTargetKeyForTarget(). May not be nil. + @result The target, or nil if no target was originally used. + */ +id _ASControlNodeTargetForTargetKey(id targetKey); + +/** + @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. + @param anEvent An even that is included in mask. + */ +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)); + +@end + +#pragma mark - +@implementation ASControlNode + +#pragma mark - Lifecycle +- (id)init +{ + if (!(self = [super init])) + return nil; + + _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. + _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; +} + +- (void)dealloc +{ + [_controlEventDispatchTable release]; + [super dealloc]; +} + +#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 _isInterestedInTouches]) + return; + + ASControlNodeEvent controlEventMask = 0; + + // 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.tracking = NO; + self.touchInside = NO; + [self cancelTrackingWithEvent:event]; + controlEventMask |= ASControlNodeEventTouchCancel; + } + 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; + + UITouch *theTouch = [touches anyObject]; + [self beginTrackingWithTouch:theTouch withEvent:event]; + + // Send the appropriate touch-down control event depending on how many times we've been tapped. + 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 _isInterestedInTouches]) + return; + + NSParameterAssert([touches count] == 1); + UITouch *theTouch = [touches anyObject]; + CGPoint touchLocation = [theTouch locationInView:self.view]; + + // Update our touchInside state. + BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil]; + + // Update our highlighted state. + CGRect expandedBounds = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset); + BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + self.touchInside = dragIsInsideExpandedBounds; + self.highlighted = dragIsInsideExpandedBounds; + + // Note we are continuing to track the touch. + [self continueTrackingWithTouch:theTouch withEvent:event]; + + [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 _isInterestedInTouches]) + return; + + // We're no longer tracking and there is no touch to be inside. + self.tracking = NO; + self.touchInside = NO; + self.highlighted = NO; + + // Note that we've cancelled tracking. + [self cancelTrackingWithEvent:event]; + + // Send the cancel event. + [self sendActionsForControlEvents:ASControlNodeEventTouchCancel + withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (![self _isInterestedInTouches]) + 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 = CGRectInset(self.view.bounds, kASControlNodeExpandedInset, kASControlNodeExpandedInset); + BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + + [self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside) + 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 _isInterestedInTouches] && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) + return NO; + + // Otherwise, go ahead. :] + return YES; +} + +#pragma mark - Control Attributes + +#pragma mark - Tracking Touches + +#pragma mark - Action Messages +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(action); + NSParameterAssert(controlEventMask != 0); + + // 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 eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableDictionary *eventDispatchTable = [_controlEventDispatchTable objectForKey:eventKey]; + // Create it if necessary. + if (!eventDispatchTable) + { + // Create the dispatch table for this event. + eventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeTargetDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries + [_controlEventDispatchTable setObject:eventDispatchTable forKey:eventKey]; + [eventDispatchTable release]; + } + + // Have we seen this target before for this event? + id targetKey = _ASControlNodeTargetKeyForTarget(target); + NSMutableArray *targetActions = [eventDispatchTable objectForKey:targetKey]; + if (!targetActions) + { + // Nope. Create an actions array for it. + targetActions = [[NSMutableArray alloc] initWithCapacity:kASControlNodeActionDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. + [eventDispatchTable setObject:targetActions forKey:targetKey]; + [targetActions release]; + } + + // Add the action message. + // Note that bizarrely enough UIControl (at least according to the docs) supports duplicate target-action pairs for a particular control event, so we replicate that behavior. + [targetActions addObject:NSStringFromSelector(action)]; + }); + + self.userInteractionEnabled = YES; +} + +- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent +{ + NSParameterAssert(target); + NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents); + + // Grab the event dispatch table for this event. + NSDictionary *eventDispatchTable = [_controlEventDispatchTable objectForKey:_ASControlNodeEventKeyForControlEvent(controlEvent)]; + if (!eventDispatchTable) + return nil; + + // Grab and return the actions for this target. + return [eventDispatchTable objectForKey:_ASControlNodeTargetKeyForTarget(target)]; +} + +- (NSSet *)allTargets +{ + NSMutableSet *targets = [[NSMutableSet alloc] init]; + + // Look at each event... + for (NSDictionary *eventDispatchTable in [_controlEventDispatchTable allValues]) + { + // and each event's targets... + for (id targetKey in eventDispatchTable) + [targets addObject:_ASControlNodeTargetForTargetKey(targetKey)]; + } + + return [targets autorelease]; +} + +- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(controlEventMask != 0); + + // 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 eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableDictionary *eventDispatchTable = [_controlEventDispatchTable objectForKey:eventKey]; + if (!eventDispatchTable) + return; + + void (^removeActionFromTarget)(id targetKey, SEL action) = ^ + (id targetKey, SEL theAction) + { + // Grab the targetActions for this target. + NSMutableArray *targetActions = [eventDispatchTable objectForKey:targetKey]; + + // Remove action if we have it. + if (theAction) + [targetActions removeObject:NSStringFromSelector(theAction)]; + // Or all actions if not. + else + [targetActions removeAllObjects]; + + // If there are no actions left, remove this target entry. + if ([targetActions count] == 0) + { + [eventDispatchTable removeObjectForKey:targetKey]; + + // If there are no targets for this event anymore, remove it. + if ([eventDispatchTable count] == 0) + [_controlEventDispatchTable removeObjectForKey:eventKey]; + } + }; + + + // Unlike addTarget:, if target is nil here we remove all targets with action. + if (!target) + { + // Look at every target, removing target-pairs that have action (or all of its actions). + for (id targetKey in eventDispatchTable) + removeActionFromTarget(targetKey, action); + } + else + removeActionFromTarget(_ASControlNodeTargetKeyForTarget(target), action); + }); +} + +#pragma mark - +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event +{ + NSParameterAssert(controlEvents != 0); + + // Enumerate the events in the mask, invoking the target-action pairs for each. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ + (ASControlNodeEvent controlEvent) + { + NSDictionary *eventDispatchTable = [_controlEventDispatchTable objectForKey:_ASControlNodeEventKeyForControlEvent(controlEvent)]; + + // For each target interested in this event... + for (id targetKey in eventDispatchTable) + { + id target = _ASControlNodeTargetForTargetKey(targetKey); + NSArray *targetActions = [eventDispatchTable objectForKey:targetKey]; + + // Invoke each of the actions on target. + for (NSString *actionMessage in targetActions) + { + SEL action = NSSelectorFromString(actionMessage); + + // Hand off to UIApplication to send the action message. + // This also handles sending to the first responder is target is nil. + [[UIApplication sharedApplication] sendAction:action to:target from:self forEvent:event]; + } + } + }); +} + +#pragma mark - Convenience +- (BOOL)_isInterestedInTouches +{ + // We're only interested in touches if we're enabled and we've got targets to talk to. + return self.enabled && ([_controlEventDispatchTable count] > 0); +} + +id _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent) +{ + return [NSNumber numberWithInteger:controlEvent]; +} + +id _ASControlNodeTargetKeyForTarget(id target) +{ + return (target ? [NSValue valueWithPointer:target] : [NSNull null]); +} + +id _ASControlNodeTargetForTargetKey(id targetKey) +{ + return (targetKey != [NSNull null] ? [(NSValue *)targetKey pointerValue] : nil); +} + +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)) +{ + // Start with our first event (touch down) and work our way up to the last event (touch cancel) + for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventTouchCancel; thisEvent <<= 1) + { + // If it's included in the mask, invoke the block. + if ((mask & thisEvent) == thisEvent) + block(thisEvent); + } +} + +#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 +{ +} + +- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ +} + +@end diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h new file mode 100644 index 0000000000..8f774eef18 --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -0,0 +1,107 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "ASAssert.h" +#import "ASThread.h" + +// +// The following methods either must or can be overriden by subclasses of ASDisplayNode. +// These methods should never be called directly by other classes. +// + +@interface ASDisplayNode (ASDisplayNodeSubclasses) + +// the view class to use when creating a new display node instance. Defaults to _ASDisplayView. ++ (Class)viewClass; + +// Returns YES if a cache node, defaults to NO +@property (nonatomic, assign, readonly, getter = isCacheNode) BOOL cacheNode; + +// Returns array of cached strict descendants (excludes self). if this is not a cacheNode, returns nil +@property (nonatomic, copy, readonly) NSArray *cachedNodes; + +// Returns the parent cache node, if any. node caching must be enabled +@property (nonatomic, assign, readonly) ASDisplayNode *superCacheNode; + +// Called on the main thread immediately after self.view is created. Best time to add gesture recognizers to the view. +- (void)didLoad; + +// Called on the main thread by the view's -layoutSubviews. Layout all subnodes or subviews in this method. +- (void)layout; + +// Called on the main thread by the view's -layoutSubviews, after -layout. Gives a chance for subclasses to perform actions after the subclass and superclass have finished laying out. +- (void)layoutDidFinish; + +// Subclasses that override should expect this method to be called on a non-main thread. The returned size is cached by +// ASDisplayNode for quick access during -layout, via -calculatedSize. Other expensive work that needs to be done +// before display can be performed here, and using ivars to cache any valuable intermediate results is encouraged. This +// method should not be called directly outside of ASDisplayNode; use -sizeToFit: or -calculatedSize instead. +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize; + +// Subclasses should call this method to invalidate the previously measured and cached size for the display node, when the contents of +// the node change in such a way as to require measuring it again. +- (void)invalidateCalculatedSize; + +// Subclasses should implement -display if the layer's contents will be set directly to an arbitrary buffer (e.g. decoded JPEG). +// Called on a background thread, some time after the view has been created. This method is called if -drawInContext: is not implemented. +- (void)display; + +// Subclasses should implement if a backing store / context is desired. Called on a background thread, some time after the view has been created. +- (void)drawInContext:(CGContextRef)ctx; + +/** + @abstract Indicates that the receiver has finished displaying. + @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) has completed. + */ +- (void)displayDidFinish; + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange; + +// Subclasses may optionally implement the touch handling methods. +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer; + +// Override to make this node respond differently to touches: hide touches from subviews, send all touches to certain subviews (hit area maximizing), etc. +// Returns a UIView, not ASDisplayNode, for two reasons: +// 1) allows sending events to plain UIViews that don't have attached nodes, 2) hitTest: is never called before the views are created. +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; + +// Subclasses should override this if they don't want their contentsScale changed. This changes an internal property +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale; + +// Recursively calls setNeedsDisplayAtScale: on subnodes. Note that only the node tree is walked, not the view or layer trees. +// Subclasses may override this if they require modifying the scale set on their child nodes. +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale; + +// Use setNeedsDisplayAtScale: and then after display, the display node will set the layer's contentsScale. This is to prevent jumps when re-rasterizing at a different contentsScale. +// Read this property if you need to know the future contentsScale of your layer, eg in drawParameters +@property (nonatomic, assign, readonly) CGFloat contentsScaleForDisplay; + +// Whether the view or layer of this display node is currently in a window +@property (nonatomic, readonly, assign, getter = isInWindow) BOOL inWindow; + +// The function that gets called for each display node in -recursiveDescription +- (NSString *)descriptionForRecursiveDescription; + +@end + +@interface ASDisplayNode (ASDisplayNodePrivate) +// 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. +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass; +@end + +#define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).isViewLoaded, @"Incorrect display node thread affinity") +#define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASDisplayNodeThreadIsMain() || !(viewNode).isViewLoaded, @"Incorrect display node thread affinity") diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h new file mode 100644 index 0000000000..8cdab893cd --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -0,0 +1,260 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "_ASAsyncTransactionContainer.h" +#import "ASBaseDefines.h" + +// Please also review ASDisplayNode+Subclasses.h if you are new to ASDisplayNode. + +@interface ASDisplayNode : NSObject + +// Designated initializer. The ASDisplayNode's view will be a subclass that enables asynchronous rendering, and passes through -layout and touch handling methods. +- (id)init; + +// Alternative initializer. Provide any UIView subclass, such as UIScrollView, and the ASDisplayNode's view will be of that type. +// If viewClass is not a subclass of ASDisplayNodeAsyncView, it will still render synchronously and -layout and touch handling methods on the node will not be called. +// The view instance will be created with alloc/init. +- (id)initWithViewClass:(Class)viewClass; + +// Alternative initializer. Provide any CALayer subclass, such as CATransformLayer, and the ASDisplayNode's view will be of that type. +// If layerClass is not a subclass of _ASDisplayLayer, it will still render synchronously and -layout on the node will not be called. +// The layer instance will be created with alloc/init. +- (id)initWithLayerClass:(Class)layerClass; + +// If this view is strictly synchronous (ie wraps a non _ASDisplayView view) +@property (nonatomic, readonly) BOOL isSynchronous; + +// The view property is lazily initialized, similar to UIViewController. +// The first access to it must be on the main thread, and should only be used on the main thread thereafter as well. +// To go the other direction, use ASViewToDisplayNode() in ASDisplayNodeExtras.h +@property (nonatomic, readonly, retain) UIView *view; +@property (atomic, readonly, assign) BOOL isViewLoaded; // Also YES if isLayerBacked == YES && self.layer != nil. Rename to isBackingLoaded? + +// If this node does not have an associated view, instead relying directly upon a layer +@property (nonatomic, assign) BOOL isLayerBacked; +// The same restrictions apply as documented above about the view property. To go the other direction, use ASLayerToDisplayNode() in ASDisplayNodeExtras.h +@property (nonatomic, readonly, retain) CALayer *layer; + +// Subclasses must not override this; it caches results from -calculateSizeThatFits:. Calling this method may be expensive if result is not cached. +// Though this method does not set the bounds of the view, it does have side effects--caching both the constraint and the result. +- (CGSize)sizeToFit:(CGSize)constrainedSize; + +// Subclasses must not override this; it returns the last cached size calculated and is never expensive. Ideal for use by subclasses in -layout, having already +// prompted their subnodes to calculate their size by calling -sizeToFit: on them in -calculateSizeThatFits: +@property (nonatomic, readonly, assign) CGSize calculatedSize; + +@property (nonatomic, readonly, assign) CGSize constrainedSizeForCalulatedSize; + +// Add a node as a subnode to this node. The subnode's view will automatically be added to this node's view automaically, lazily if the views are not created yet. +- (void)addSubnode:(ASDisplayNode *)subnode; + +// Insert a subnode before a given subnode in the list. If the views are loaded, the subnode's view will be inserted below the given node's view in the hierarchy even if there are other non-displaynode views. +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below; + +// Insert a subnode after a given subnode in the list. If the views are loaded, the subnode's view will be inserted above the given node's view in the hierarchy even if there are other non-displaynode views. +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)below; + +// Insert a subnode at a given index in subnodes. If this node's view is loaded, ASDisplayNode insert the subnode's view after the subnode at index - 1's view even if there are other non-displaynode views. +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx; + +/** + Replace subnode with replacementSubnode. + + If subnode is not a subnode of self, this method will throw an exception + If replacementSubnode is nil, this method will throw an exception + Should both subnode and replacementSubnode already be subnodes of self, subnode is removed and replacementSubnode inserted in its place. + @param subnode a subnode of self + @param replacementSubnode a node with which to replace subnode + */ +- (void)replaceSubnode:(ASDisplayNode *)subnode withSubnode:(ASDisplayNode *)replacementSubnode; + +/** + Add a subnode, but have it size asynchronously on a background queue. + @param subnode The unsized subnode to insert into the view hierarchy + @param completion The completion callback will be called on the main queue after the subnode has been inserted in place of the placeholder. + @return A placeholder node is inserted into the hierarchy where the node will be. The placeholder can be moved around in the hiercharchy while the view is sizing. Once sizing is complete on the background queue, this placeholder will be removed and the + */ +- (ASDisplayNode *)addSubnodeAsynchronously:(ASDisplayNode *)subnode + completion:(void(^)(ASDisplayNode *replacement))completion; + +- (void)replaceSubnodeAsynchronously:(ASDisplayNode *)subnode + withNode:(ASDisplayNode *)replacementSubnode + completion:(void(^)(BOOL cancelled, ASDisplayNode *replacement, ASDisplayNode *oldSubnode))completion; + +// Remove this node from its supernode. The node's view will be automatically removed from the supernode's view. +- (void)removeFromSupernode; + +// Access to the subnodes of this node, and the supernode of this node. +@property (nonatomic, readonly, retain) NSArray *subnodes; +@property (nonatomic, readonly, assign) ASDisplayNode *supernode; + +// Called just before the view is added to a superview. +// TODO rename these to the UIView selectors, willMoveToSuperview etc +- (void)willAppear; + +// Called after the view is removed from the window +- (void)willDisappear; + +// Called after the view is removed from the window +- (void)didDisappear; + +/** + @abstract + Set whether this node's view performs asynchronous rendering. Defaults to YES, except + for synchronous views (ie, those created with -initWithViewClass: / -initWithLayerClass:), which are always NO + + @discussion + If this flag is set, then the node will participate in the current asyncdisplaykit_async_transaction and do its rendering on the displayQueue instead of the main thread. + Asynchronous rendering proceeds as follows: + + When the view is initially added to the hierarchy, it has -needsDisplay true. + After layout, Core Animation will call -display on the _ASDisplayLayer + -display enqueues a rendering operation on the displayQueue + When the render block executes, it calls the delegate display method (-drawRect:... or -display) + The delegate provides contents via this method and an operation is added to the asyncdisplaykit_async_transaction + Once all rendering is complete for the current asyncdisplaykit_async_transaction, + the completion for the block sets the contents on all of the layers in the same frame + + If asynchronous rendering is disabled: + + When the view is initially added to the hierarchy, it has -needsDisplay true. + After layout, Core Animation will call -display on the _ASDisplayLayer + -display calls delegate display method (-drawRect:... or -display) immediately + -display sets the layer contents immediately with the result + + Note: this has nothing to do with CALayer@drawsAsynchronously + */ +@property (nonatomic) BOOL displaysAsynchronously; + +/** + @abstract + When set to YES, causes all descendant nodes' layers/views to be drawn directly into this node's layer/view's backing store. Defaults to NO. + + @discussion + If a node's descendants are static (never animated or never change attributes after creation) then that node is a good candidate for rasterization. Rasterizing descendants has two main benefits: + 1) Backing stores for descendant layers are not created. Instead the layers are drawn directly into the rasterized container. This can save a great deal of memory. + 2) Since the entire subtree is drawn into one backing store, compositing and blending are eliminated in that subtree which can help improve animation/scrolling/etc performance. + + Rasterization does not currently support descendants with transform, sublayerTransform, or alpha. Those properties will be ignored when rasterizing descendants. + + Note: this has nothing to do with -[CALayer shouldRasterize], which doesn't work with ASDisplayNode's asynchronous rendering model. + */ +@property (nonatomic, assign) BOOL shouldRasterizeDescendants; + +// Call this to display the node's view/layer immediately on the current thread, bypassing the background thread rendering. +- (void)displayImmediately; + +// Set this to YES to prevent the node's layer from displaying. A subclass may check this flag during -display or -drawInContext: to cancel a display +// that is already in progress. See -displayWasCancelled. +// If a setNeedsDisplay occurs while preventOrCancelDisplay is YES, and preventOrCancelDisplay is set to NO, then the layer will be automatically +// displayed. Defaults to NO. Does not control display for any child or descendant nodes; for that, use -recursiveSetPreventOrCancelDisplay:. +@property (nonatomic, assign) BOOL preventOrCancelDisplay; + +// Same as 'preventOrCancelDisplay' but also affects all child and descendant nodes. +- (void)recursiveSetPreventOrCancelDisplay:(BOOL)flag; + +// When set to a non-zero inset, increases the bounds for hit testing to make it easier to tap or perform gestures on this node. Default is UIEdgeInsetsZero. +// This affects the default implementation of -hitTest and -pointInside, so subclasses should call super if you override it and want hitTestSlop applied. +@property (nonatomic, assign) UIEdgeInsets hitTestSlop; + +// Helper method for computing whether a point falls within the node's bounds. Includes the "slop" factor specified with hitTestSlop. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; + +// Coordinate space mapping that works before UIView instantiation +- (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node; +- (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node; +- (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node; +- (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node; + +@end + +@interface ASDisplayNode (Debugging) + +// A nice way to print your view hieararchy for debugging: (lldb) po [node recursiveDescription] +- (NSString *)displayNodeRecursiveDescription; + +@end + +// +// The following properties and methods provide thread-safe access to traditionally unsafe UIView and CALayer functionality. +// Using them will not cause the actual view/layer to be created, and will be applied when it is created (when the view or layer property is accessed). +// After the view is created, the properties pass through to the view directly as if called on the main thread. +// See UIView.h and CALayer.h for documentation on these common properties. +// + +@interface ASDisplayNode (UIViewBridge) + +- (void)setNeedsDisplay; // Marks the view as needing display. Convenience for use whether view is created or not, or from a background thread. +- (void)setNeedsLayout; // Marks the view as needing layout. Convenience for use whether view is created or not, or from a background thread. + +@property (atomic, retain) id contents; // default=nil +@property (atomic, assign) BOOL clipsToBounds; // default==NO +@property (atomic, getter=isOpaque) BOOL opaque; // default==YES + +@property (atomic, assign) BOOL allowsEdgeAntialiasing; +@property (atomic, assign) unsigned int edgeAntialiasingMask; // default==all values from CAEdgeAntialiasingMask + +@property (atomic, getter=isHidden) BOOL hidden; // default==NO +@property (atomic, assign) BOOL needsDisplayOnBoundsChange;// default==NO +@property (atomic, assign) BOOL autoresizesSubviews; // default==YES (undefined for layer-backed nodes) +@property (atomic, assign) UIViewAutoresizing autoresizingMask; // default==UIViewAutoresizingNone (undefined for layer-backed nodes) +@property (atomic, assign) CGFloat alpha; // default=1.0f +@property (atomic, assign) CGRect bounds; // default=CGRectZero +@property (atomic, assign) CGRect frame; // default=CGRectZero +@property (atomic, assign) CGPoint anchorPoint; // default={0.5, 0.5} +@property (atomic, assign) CGFloat zPosition; // default=0.0 +@property (atomic, assign) CGPoint position; // default=CGPointZero +@property (atomic, assign) CGFloat contentsScale; // default=1.0f. See @contentsScaleForDisplay for more info +@property (atomic, assign) CATransform3D transform; // default=CATransform3DIdentity +@property (atomic, assign) CATransform3D subnodeTransform; // default=CATransform3DIdentity +@property (atomic, copy) NSString *name; // default=nil. Use this to tag your layers in the server-recurse-description / pca or for your own purposes + +/** + In contrast to UIView, setting a transparent color will not set opaque = NO. + This only affects nodes that implement +drawRect like ASTextNode +*/ +@property (atomic, retain) UIColor *backgroundColor; // default=nil +/** + This is like UIView's contentMode property, but better. We do our own mapping to layer.contentsGravity in _ASDisplayView you can set needsDisplayOnBoundsChange independently. Thus, UIViewContentModeRedraw is not allowed; use needsDisplayOnBoundsChange = YES instead, and pick an appropriate contentMode for your content while it's being re-rendered. + */ +@property (atomic, assign) UIViewContentMode contentMode; // default=UIViewContentModeScaleToFill + +@property (atomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; // default=YES (NO for layer-backed nodes) +@property (atomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO +@property (atomic, assign) CGColorRef shadowColor; // default=opaque rgb black +@property (atomic, assign) CGFloat shadowOpacity; // default=0.0 +@property (atomic, assign) CGSize shadowOffset; // default=(0, -3) +@property (atomic, assign) CGFloat shadowRadius; // default=3 +@property (atomic, assign) CGFloat borderWidth; // default=0 +@property (atomic, assign) CGColorRef borderColor; // default=opaque rgb black + + +/** + Accessibility support + */ +@property (atomic, assign) BOOL isAccessibilityElement; +@property (atomic, copy) NSString *accessibilityLabel; +@property (atomic, copy) NSString *accessibilityHint; +@property (atomic, copy) NSString *accessibilityValue; +@property (atomic, assign) UIAccessibilityTraits accessibilityTraits; +@property (atomic, assign) CGRect accessibilityFrame; +@property (atomic, retain) NSString *accessibilityLanguage; +@property (atomic, assign) BOOL accessibilityElementsHidden; +@property (atomic, assign) BOOL accessibilityViewIsModal; +@property (atomic, assign) BOOL shouldGroupAccessibilityChildren; + +@end + +/* + ASDisplayNode participates in ASAsyncTransactions, so you can determine when your subnodes are done rendering. + See: -(void)asyncdisplaykit_asyncTransactionContainerStateDidChange in ASDisplayNodeSubclass.h + */ +@interface ASDisplayNode (ASDisplayNodeAsyncTransactionContainer) +@end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm new file mode 100644 index 0000000000..9180eead5d --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -0,0 +1,1621 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDisplayNode.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNodeInternal.h" + +#import + +#import "_ASAsyncTransaction.h" +#import "_ASPendingState.h" +#import "_ASDisplayView.h" +#import "_ASScopeTimer.h" +#import "ASDisplayNodeExtras.h" + +@interface ASDisplayNode () + +/** + * + * See ASDisplayNodeInternal.h for ivars + * + */ + +@end + +// Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) +#if TIME_DISPLAYNODE_OPS +#define TIME_SCOPED(outVar) ASDN::ScopeTimer t(outVar) +#else +#define TIME_SCOPED(outVar) +#endif + +@implementation ASDisplayNode + +BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) +{ + Method superclassMethod = class_getInstanceMethod([ASDisplayNode class], selector); + Method subclassMethod = class_getInstanceMethod(subclass, selector); + IMP superclassIMP = superclassMethod ? method_getImplementation(superclassMethod) : NULL; + IMP subclassIMP = subclassMethod ? method_getImplementation(subclassMethod) : NULL; + + return (superclassIMP != subclassIMP); +} + ++ (void)initialize +{ + if (self == [ASDisplayNode class]) { + return; + } + + // Subclasses should never override these + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(sizeToFit:)), @"Subclass %@ must not override sizeToFit method", NSStringFromClass(self)); +} + ++ (BOOL)layerBackedNodesEnabled +{ + return YES; +} + ++ (Class)viewClass +{ + return [_ASDisplayView class]; +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - NSObject Overrides + +- (id)initWithViewClass:(Class)viewClass +{ + if (!(self = [self init])) + return nil; + + ASDisplayNodeAssert([viewClass isSubclassOfClass:[UIView class]], @"should initialize with a subclass of UIView"); + _viewClass = [viewClass retain]; + _flags.isSynchronous = ![viewClass isSubclassOfClass:[_ASDisplayView class]]; + + return self; +} + +- (id)initWithLayerClass:(Class)layerClass +{ + if (!(self = [self init])) + return nil; + + ASDisplayNodeAssert([layerClass isSubclassOfClass:[CALayer class]], @"should initialize with a subclass of CALayer"); + + _layerClass = [layerClass retain]; + _flags.isSynchronous = ![layerClass isSubclassOfClass:[_ASDisplayLayer class]]; + + _flags.isLayerBacked = YES; + + return self; +} + +- (id)init +{ + self = [super init]; + if (!self) return nil; + + _contentsScaleForDisplay = [[UIScreen mainScreen] scale]; + + _displaySentinel = [[ASSentinel alloc] init]; + + _flags.inWindow = NO; + _flags.displaysAsynchronously = YES; + + _flags.implementsDisplay = [[self class] respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] || [self.class respondsToSelector:@selector(displayWithParameters:isCancelled:)]; + + _flags.hasWillDisplayAsyncLayer = ([self respondsToSelector:@selector(willDisplayAsyncLayer:)] ? 1 : 0); + _flags.hasClassDisplay = ([[self class] respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); + _flags.hasDrawParametersForAsyncLayer = ([self respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + + return self; +} + +#if __has_feature(objc_arc) +#warning This file must be compiled without ARC. Use -fno-objc-arc (or convert project to MRR). +#endif + +#if !__has_feature(objc_arc) +_OBJC_SUPPORTED_INLINE_REFCNT_WITH_DEALLOC2MAIN(_retainCount); +#endif + +- (void)dealloc +{ + ASDisplayNodeAssertMainThread(); + + self.asyncLayer.asyncDelegate = nil; + _view.asyncdisplaykit_node = nil; + _layer.asyncdisplaykit_node = nil; + + // Remove any subnodes so they lose their connection to the now deallocated parent. This can happen + // because subnodes do not retain their supernode, but subnodes can legitimately remain alive if another + // thing outside the view hierarchy system (e.g. async display, controller code, etc). keeps a retained + // reference to subnodes. + + for (ASDisplayNode *subnode in _subnodes) + [subnode __setSupernode:nil]; + + [_viewClass release]; + [_subnodes release]; + + [_view release]; + _view = nil; + _subnodes = nil; + if (_flags.isLayerBacked) { + _layer.delegate = nil; + } + [_layer release]; + _layer = nil; + + [self __setSupernode:nil]; + [_pendingViewState release]; + _pendingViewState = nil; + [_replaceAsyncSentinel release]; + _replaceAsyncSentinel = nil; + + [_displaySentinel release]; + _displaySentinel = nil; + + [super dealloc]; +} + +#pragma mark - UIResponder overrides + +- (UIResponder *)nextResponder +{ + return self.view.superview; +} + +#pragma mark - Core + +- (ASDisplayNode *)__rasterizedContainerNode +{ + ASDisplayNode *node = self.supernode; + while (node) { + if (node.shouldRasterizeDescendants) { + return node; + } + node = node.supernode; + } + + return nil; +} + +- (BOOL)__shouldLoadViewOrLayer +{ + return ![self __rasterizedContainerNode]; +} + +- (BOOL)__shouldSize +{ + return YES; +} + +- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked +{ + ASDN::MutexLocker l(_propertyLock); + + if (self._isDeallocating) { + return; + } + + if (![self __shouldLoadViewOrLayer]) { + return; + } + + if (isLayerBacked) { + TIME_SCOPED(_debugTimeToCreateView); + if (!_layerClass) { + _layerClass = [self.class layerClass]; + } + + _layer = [[_layerClass alloc] init]; + _layer.delegate = self; + } else { + TIME_SCOPED(_debugTimeToCreateView); + if (!_viewClass) { + _viewClass = [self.class viewClass]; + } + _view = [[_viewClass alloc] init]; + _view.asyncdisplaykit_node = self; + _layer = [_view.layer retain]; + } + _layer.asyncdisplaykit_node = self; +#if DEBUG + _layer.name = self.description; +#endif + self.asyncLayer.asyncDelegate = self; + + { + TIME_SCOPED(_debugTimeToApplyPendingState); + [self _applyPendingStateToViewOrLayer]; + } + { + TIME_SCOPED(_debugTimeToAddSubnodeViews); + [self _addSubnodeViewsAndLayers]; + } + { + TIME_SCOPED(_debugTimeForDidLoad); + [self didLoad]; + } +} + +- (UIView *)view +{ + ASDisplayNodeAssert(!_flags.isLayerBacked, @"Call to -view undefined on layer-backed nodes"); + if (_flags.isLayerBacked) { + return nil; + } + if (!_view) { + ASDisplayNodeAssertMainThread(); + [self _loadViewOrLayerIsLayerBacked:NO]; + } + return _view; +} + +- (CALayer *)layer +{ + if (!_layer) { + ASDisplayNodeAssertMainThread(); + + if (!_flags.isLayerBacked) { + return self.view.layer; + } + [self _loadViewOrLayerIsLayerBacked:YES]; + } + return _layer; +} + +// Returns nil if our view is not an _ASDisplayView, but will create it if necessary. +- (_ASDisplayView *)ensureAsyncView +{ + return _flags.isSynchronous ? nil:(_ASDisplayView *)self.view; +} + +// Returns nil if the layer is not an _ASDisplayLayer; will not create the view if nil +- (_ASDisplayLayer *)asyncLayer +{ + ASDN::MutexLocker l(_propertyLock); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} + +- (BOOL)isViewLoaded +{ + ASDN::MutexLocker l(_propertyLock); + return (_view != nil || (_flags.isLayerBacked && _layer != nil)); +} + +- (BOOL)isSynchronous +{ + return _flags.isSynchronous; +} + +- (void)setIsSynchronous:(BOOL)flag +{ + _flags.isSynchronous = flag; +} + +- (void)setIsLayerBacked:(BOOL)isLayerBacked +{ + if (![self.class layerBackedNodesEnabled]) return; + + ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssert(!_view && !_layer, @"Cannot change isLayerBacked after layer or view has loaded"); + if (isLayerBacked != _flags.isLayerBacked && !_view && !_layer) { + _flags.isLayerBacked = isLayerBacked; + } +} + +- (BOOL)isLayerBacked +{ + ASDN::MutexLocker l(_propertyLock); + return _flags.isLayerBacked; +} + +#pragma mark - + +- (CGSize)sizeToFit:(CGSize)constrainedSize +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (![self __shouldSize]) + return CGSizeZero; + + // only calculate the size if + // - we haven't already + // - the width is different from the last time + // - the height is different from the last time + if (!_flags.sizeCalculated || !CGSizeEqualToSize(constrainedSize, _constrainedSize)) { + _size = [self calculateSizeThatFits:constrainedSize]; + _constrainedSize = constrainedSize; + _flags.sizeCalculated = YES; + } + + ASDisplayNodeAssertTrue(_size.width >= 0.0); + ASDisplayNodeAssertTrue(_size.height >= 0.0); + return _size; +} + +- (BOOL)displaysAsynchronously +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + if (self.isSynchronous) { + return NO; + } else { + return _flags.displaysAsynchronously; + } +} + +- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously +{ + ASDisplayNodeAssertThreadAffinity(self); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (_flags.isSynchronous) + return; + + ASDN::MutexLocker l(_propertyLock); + + if (_flags.displaysAsynchronously == displaysAsynchronously) + return; + + _flags.displaysAsynchronously = displaysAsynchronously; + + self.asyncLayer.displaysAsynchronously = displaysAsynchronously; +} + +- (BOOL)shouldRasterizeDescendants +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _flags.shouldRasterizeDescendants; +} + +- (void)setShouldRasterizeDescendants:(BOOL)flag +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (_flags.shouldRasterizeDescendants == flag) + return; + + _flags.shouldRasterizeDescendants = flag; +} + +- (CGFloat)contentsScaleForDisplay +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + return _contentsScaleForDisplay; +} + +- (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (_contentsScaleForDisplay == contentsScaleForDisplay) + return; + + _contentsScaleForDisplay = contentsScaleForDisplay; +} + +- (void)displayImmediately +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isSynchronous, @"this method is designed for asynchronous mode only"); + + [[self asyncLayer] displayImmediately]; +} + +// These private methods ensure that subclasses are not required to call super in order for _renderingSubnodes to be properly managed. + +- (void)__layout +{ + ASDisplayNodeAssertMainThread(); + ASDN::MutexLocker l(_propertyLock); + if (CGRectEqualToRect(_layer.bounds, CGRectZero)) + return; // Performing layout on a zero-bounds view often results in frame calculations with negative sizes after applying margins, which will cause sizeToFit: on subnodes to assert. + [self layout]; + [self layoutDidFinish]; +} + +- (void)layoutDidFinish +{ +} + +- (CATransform3D)_transformToAncestor:(ASDisplayNode *)ancestor +{ + CATransform3D transform = CATransform3DIdentity; + ASDisplayNode *currentNode = self; + while (currentNode.supernode) { + if (currentNode == ancestor) { + return transform; + } + + CGPoint anchorPoint = currentNode.anchorPoint; + CGRect bounds = currentNode.bounds; + CGPoint position = currentNode.position; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + + transform = CATransform3DTranslate(transform, origin.x, origin.y, 0); + transform = CATransform3DTranslate(transform, -bounds.origin.x, -bounds.origin.y, 0); + currentNode = currentNode.supernode; + } + return transform; +} + +static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendent) +{ + ASDisplayNode *supernode = possibleDescendent; + while (supernode) { + if (supernode == possibleAncestor) { + return YES; + } + supernode = supernode.supernode; + } + + return NO; +} + +/** + * NOTE: It is an error to try to convert between nodes which do not share a common ancestor. This behavior is + * disallowed in UIKit documentation and the behavior is left undefined. The output does not have a rigorously defined + * failure mode (i.e. returning CGPointZero or returning the point exactly as passed in). Rather than track the internal + * undefined and undocumented behavior of UIKit in ASDisplayNode, this operation is defined to be incorrect in all + * circumstances and must be fixed wherever encountered. + */ +static inline 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; +} + +static inline ASDisplayNode *_getRootNode(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; +} + +static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNode *referenceNode, ASDisplayNode *targetNode) +{ + ASDisplayNode *ancestor = _ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); + + // Transform into global (away from reference coordinate space) + CATransform3D transformToGlobal = [referenceNode _transformToAncestor:ancestor]; + + // Transform into local (via inverse transform from target to ancestor) + CATransform3D transformToLocal = CATransform3DInvert([targetNode _transformToAncestor:ancestor]); + + return CATransform3DConcat(transformToGlobal, transformToLocal); +} + +- (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + // Get root node of the accessible node hierarchy, if node not specified + node = node ? node : _getRootNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + // Get root node of the accessible node hierarchy, if node not specified + node = node ? node : _getRootNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + // Get root node of the accessible node hierarchy, if node not specified + node = node ? node : _getRootNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + // Get root node of the accessible node hierarchy, if node not specified + node = node ? node : _getRootNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +#pragma mark - _ASDisplayLayerDelegate + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + // Subclass hook. + [self displayDidFinish]; +} + +#pragma mark - CALayerDelegate + +// We are only the delegate for the layer when we are layer-backed, as UIView performs this funcition normally +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + if (event == kCAOnOrderIn) { + [self __appear]; + } else if (event == kCAOnOrderOut) { + [self __disappear]; + } + + ASDisplayNodeAssert(_flags.isLayerBacked, @"We shouldn't get called back here if there is no layer"); + return (id)[NSNull null]; +} + +#pragma mark - + +static bool disableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASDisplayNode *to) +{ + if (!from || !to) return NO; + if (from->_flags.isSynchronous) return NO; + if (to->_flags.isSynchronous) return NO; + if (from->_flags.inWindow != to->_flags.inWindow) return NO; + return YES; +} + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + ASDisplayNode *oldParent = subnode.supernode; + if (!subnode || subnode == self || oldParent == self) + return; + + // Disable appearance methods during move between supernodes, but make sure we restore their state after we do our thing + BOOL isMovingEquivalentParents = disableNotificationsForMovingBetweenParents(oldParent, self); + if (isMovingEquivalentParents) { + [subnode __incrementVisibilityNotificationsDisabled]; + } + [subnode removeFromSupernode]; + + if (!_subnodes) + _subnodes = [[NSMutableArray alloc] init]; + + [_subnodes addObject:subnode]; + + if (self.isViewLoaded) { + // If this node has a view or layer, force the subnode to also create its view or layer and add it to the hierarchy here. + // Otherwise there is no way for the subnode's view or layer to enter the hierarchy, except recursing down all + // subnodes on the main thread after the node tree has been created but before the first display (which + // could introduce performance problems). + if (ASDisplayNodeThreadIsMain()) { + [self _addSubnodeSubviewOrSublayer:subnode]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [self _addSubnodeSubviewOrSublayer:subnode]; + }); + } + } + + ASDisplayNodeAssert(isMovingEquivalentParents == disableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); + if (isMovingEquivalentParents) { + [subnode __decrementVisibilityNotificationsDisabled]; + } + + [subnode __setSupernode:self]; +} + +/* + Private helper function. + You must hold _propertyLock to call this. + + @param subnode The subnode to insert + @param subnodeIndex The index in _subnodes to insert it + @param viewSublayerIndex The index in layer.sublayers (not view.subviews) at which to insert the view (use if we can use the view API) otherwise pass NSNotFound + @param sublayerIndex The index in layer.sublayers at which to insert the layer (use if either parent or subnode is layer-backed) otherwise pass NSNotFound + @param oldSubnode Remove this subnode before inserting; ok to be nil if no removal is desired + */ +- (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnodeIndex sublayerIndex:(NSInteger)sublayerIndex andRemoveSubnode:(ASDisplayNode *)oldSubnode +{ + if (subnodeIndex == NSNotFound) + return; + + [subnode retain]; + + ASDisplayNode *oldParent = [subnode _deallocSafeSupernode]; + // Disable appearance methods during move between supernodes, but make sure we restore their state after we do our thing + BOOL isMovingEquivalentParents = disableNotificationsForMovingBetweenParents(oldParent, self); + if (isMovingEquivalentParents) { + [subnode __incrementVisibilityNotificationsDisabled]; + } + [subnode removeFromSupernode]; + + if (!_subnodes) + _subnodes = [[NSMutableArray alloc] init]; + + [oldSubnode removeFromSupernode]; + [_subnodes insertObject:subnode atIndex:subnodeIndex]; + + // Don't bother inserting the view/layer if in a rasterized subtree, becuase there are no layers in the hierarchy and none of this could possibly work. + if (!_flags.shouldRasterizeDescendants && ![self __rasterizedContainerNode]) { + if (_layer) { + ASDisplayNodeCAssertMainThread(); + + ASDisplayNodeAssert(sublayerIndex != NSNotFound, @"Should pass either a valid sublayerIndex"); + + if (sublayerIndex != NSNotFound) { + BOOL canUseViewAPI = !subnode.isLayerBacked && !self.isLayerBacked; + // If we can use view API, do. Due to an apple bug, -insertSubview:atIndex: actually wants a LAYER index, which we pass in + if (canUseViewAPI && sublayerIndex != NSNotFound) { + [_view insertSubview:subnode.view atIndex:sublayerIndex]; + } else if (sublayerIndex != NSNotFound) { + [_layer insertSublayer:subnode.layer atIndex:sublayerIndex]; + } + } + } + } + + ASDisplayNodeAssert(isMovingEquivalentParents == disableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); + if (isMovingEquivalentParents) { + [subnode __decrementVisibilityNotificationsDisabled]; + } + + [subnode __setSupernode:self]; + [subnode release]; +} + +- (void)replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (!replacementSubnode || [oldSubnode _deallocSafeSupernode] != self) { + ASDisplayNodeAssert(0, @"Bad use of api. Invalid subnode to replace async."); + return; + } + + ASDisplayNodeAssert(!(self.isViewLoaded && !oldSubnode.isViewLoaded), @"ASDisplayNode corruption bug. We have view loaded, but child node does not."); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + NSInteger subnodeIndex = [_subnodes indexOfObjectIdenticalTo:oldSubnode]; + NSInteger sublayerIndex = NSNotFound; + + if (_layer) { + sublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:oldSubnode.layer]; + ASDisplayNodeAssert(sublayerIndex != NSNotFound, @"Somehow oldSubnode's supernode is self, yet we could not find it in our layers to replace"); + if (sublayerIndex == NSNotFound) return; + } + + [self _insertSubnode:replacementSubnode atSubnodeIndex:subnodeIndex sublayerIndex:sublayerIndex andRemoveSubnode:oldSubnode]; +} + +// This is just a convenience to avoid a bunch of conditionals +static NSInteger incrementIfFound(NSInteger i) { + return i == NSNotFound ? NSNotFound : i + 1; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + ASDisplayNodeAssert(subnode, @"Cannot insert a nil subnode"); + if (!subnode) + return; + + ASDisplayNodeAssert([below _deallocSafeSupernode] == self, @"Node to insert below must be a subnode"); + if ([below _deallocSafeSupernode] != self) + return; + + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + NSInteger belowSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:below]; + NSInteger belowSublayerIndex = NSNotFound; + + if (_layer) { + belowSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:below.layer]; + ASDisplayNodeAssert(belowSublayerIndex != NSNotFound, @"Somehow below's supernode is self, yet we could not find it in our layers to reference"); + if (belowSublayerIndex == NSNotFound) + return; + } + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to insert it will mess up our calculation + if ([subnode _deallocSafeSupernode] == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes < belowSubnodeIndex) { + belowSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers < belowSublayerIndex) { + belowSublayerIndex--; + } + } + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find below in subnodes"); + + [self _insertSubnode:subnode atSubnodeIndex:belowSubnodeIndex sublayerIndex:belowSublayerIndex andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + ASDisplayNodeAssert(subnode, @"Cannot insert a nil subnode"); + if (!subnode) + return; + + ASDisplayNodeAssert([above _deallocSafeSupernode] == self, @"Node to insert above must be a subnode"); + if ([above _deallocSafeSupernode] != self) + return; + + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + NSInteger aboveSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:above]; + NSInteger aboveSublayerIndex = NSNotFound; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, becuase there are no layers in the hierarchy and none of this could possibly work. + if (!_flags.shouldRasterizeDescendants && ![self __rasterizedContainerNode]) { + if (_layer) { + aboveSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:above.layer]; + ASDisplayNodeAssert(aboveSublayerIndex != NSNotFound, @"Somehow above's supernode is self, yet we could not find it in our layers to replace"); + if (aboveSublayerIndex == NSNotFound) + return; + } + ASDisplayNodeAssert(aboveSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to insert it will mess up our calculation + if ([subnode _deallocSafeSupernode] == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes <= aboveSubnodeIndex) { + aboveSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers <= aboveSublayerIndex) { + aboveSublayerIndex--; + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:incrementIfFound(aboveSubnodeIndex) sublayerIndex:incrementIfFound(aboveSublayerIndex) andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (idx > _subnodes.count || idx < 0) { + NSString *reason = [NSString stringWithFormat:@"Cannot insert a subnode at index %d. Count is %d", idx, _subnodes.count]; + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; + } + + NSInteger sublayerIndex = NSNotFound; + + // Account for potentially having other subviews + if (_layer && idx == 0) { + sublayerIndex = 0; + } else if (_layer) { + ASDisplayNode *positionInRelationTo = (_subnodes.count > 0 && idx > 0) ? _subnodes[idx - 1] : nil; + if (positionInRelationTo) { + sublayerIndex = incrementIfFound([_layer.sublayers indexOfObjectIdenticalTo:positionInRelationTo.layer]); + } + } + + [self _insertSubnode:subnode atSubnodeIndex:idx sublayerIndex:sublayerIndex andRemoveSubnode:nil]; +} + + +- (void)_addSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(self.isViewLoaded, @"_addSubnodeSubview: should never be called before our own view is created"); + + BOOL canUseViewAPI = !self.isLayerBacked && !subnode.isLayerBacked; + if (canUseViewAPI) { + [_view addSubview:subnode.view]; + } else { + // Disallow subviews in a layer-backed node + ASDisplayNodeAssert(subnode.isLayerBacked, @"Cannot add a subview to a layer-backed node; only sublayers permitted."); + [_layer addSublayer:subnode.layer]; + } +} + +- (void)_addSubnodeViewsAndLayers +{ + ASDisplayNodeAssertMainThread(); + + for (ASDisplayNode *node in [[_subnodes copy] autorelease]) { + [self _addSubnodeSubviewOrSublayer:node]; + } +} + +- (void)_removeSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + // Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe. + // The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method. + if (!subnode || [subnode _deallocSafeSupernode] != self) + return; + + [_subnodes removeObjectIdenticalTo:subnode]; + + [subnode __setSupernode:nil]; +} + +- (void)removeFromSupernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + if (!_supernode) + return; + + // Do this before removing the view from the hierarchy, as the node will clear its supernode pointer when its view is removed from the hierarchy. + [_supernode _removeSubnode:self]; + + if (ASDisplayNodeThreadIsMain()) { + if (_flags.isLayerBacked) { + [_layer removeFromSuperlayer]; + } else { + [_view removeFromSuperview]; + } + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + if (_flags.isLayerBacked) { + [_layer removeFromSuperlayer]; + } else { + [_view removeFromSuperview]; + } + }); + } +} + +- (BOOL)__visibilityNotificationsDisabled +{ + ASDN::MutexLocker l(_propertyLock); + return _flags.visibilityNotificationsDisabled > 0; +} + +- (void)__incrementVisibilityNotificationsDisabled +{ + ASDN::MutexLocker l(_propertyLock); + const size_t maxVisibilityIncrement = (1ULL< 0, @"Can't decrement past 0"); + if (_flags.visibilityNotificationsDisabled > 0) + _flags.visibilityNotificationsDisabled--; +} + +// This uses the layer hieararchy for safety. Who knows what people might do and it would be bad to have visibilty out of sync +- (BOOL)__hasParentWithVisibilityNotificationsDisabled +{ + CALayer *layer = _layer; + do { + ASDisplayNode *node = ASLayerToDisplayNode(layer); + if (node) { + if (node->_flags.visibilityNotificationsDisabled) { + return YES; + } + } + layer = layer.superlayer; + } while (layer); + + return NO; +} + +- (void)__appear +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isInAppear, @"Should not cause recursive __appear"); + if (!self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { + self.inWindow = YES; + _flags.isInAppear = YES; + if (self.shouldRasterizeDescendants) { + // Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants. + [self _recursiveWillAppear]; + } else { + [self willAppear]; + } + _flags.isInAppear = NO; + } +} + +- (void)__disappear +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isInDisappear, @"Should not cause recursive __disappear"); + if (self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { + self.inWindow = NO; + + [self.asyncLayer cancelAsyncDisplay]; + + _flags.isInDisappear = YES; + if (self.shouldRasterizeDescendants) { + // Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants. + [self _recursiveWillDisappear]; + } else { + [self willDisappear]; + } + + if (self.shouldRasterizeDescendants) { + // Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants. + [self _recursiveDidDisappear]; + } else { + [self didDisappear]; + } + + _flags.isInDisappear = NO; + } +} + +- (void)_recursiveWillAppear +{ + if (_flags.visibilityNotificationsDisabled) { + return; + } + + _flags.isInAppear = YES; + [self willAppear]; + _flags.isInAppear = NO; + + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursiveWillAppear]; + } +} + +- (void)_recursiveWillDisappear +{ + if (_flags.visibilityNotificationsDisabled) { + return; + } + + _flags.isInDisappear = YES; + [self willDisappear]; + _flags.isInDisappear = NO; + + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursiveWillDisappear]; + } +} + +- (void)_recursiveDidDisappear +{ + if (_flags.visibilityNotificationsDisabled) { + return; + } + + _flags.isInDisappear = YES; + [self didDisappear]; + _flags.isInDisappear = NO; + + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursiveDidDisappear]; + } +} + +- (NSArray *)subnodes +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return [[_subnodes copy] autorelease]; +} + +- (ASDisplayNode *)supernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return [[_supernode retain] autorelease]; +} + +// This is a thread-method to return the supernode without causing it to be retained autoreleased. See -_removeSubnode: for details. +- (ASDisplayNode *)_deallocSafeSupernode +{ + ASDN::MutexLocker l(_propertyLock); + return _supernode; +} + +- (void)__setSupernode:(ASDisplayNode *)supernode +{ + ASDN::MutexLocker l(_propertyLock); + _supernode = supernode; +} + +#pragma mark - For Subclasses + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssertThreadAffinity(self); + return CGSizeZero; +} + +- (CGSize)calculatedSize +{ + ASDisplayNodeAssertThreadAffinity(self); + return _size; +} + +- (CGSize)constrainedSizeForCalulatedSize +{ + ASDisplayNodeAssertThreadAffinity(self); + return _constrainedSize; +} + +- (void)invalidateCalculatedSize +{ + ASDisplayNodeAssertThreadAffinity(self); + // This will cause -sizeToFit: to actually compute the size instead of returning the previously cached size + _flags.sizeCalculated = NO; +} + +- (void)didLoad +{ + ASDisplayNodeAssertMainThread(); +} + +- (void)willAppear +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isInAppear, @"You should never call -willAppear directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isInDisappear, @"ASDisplayNode inconsistency. __appear and __disappear are mutually exclusive"); +} + +- (void)willDisappear +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isInDisappear, @"You should never call -willDisappear directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isInAppear, @"ASDisplayNode inconsistency. __appear and __disappear are mutually exclusive"); +} + +- (void)didDisappear +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isInDisappear, @"You should never call -didDisappear directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isInAppear, @"ASDisplayNode inconsistency. __appear and __disappear are mutually exclusive"); +} + +- (void)layout +{ + ASDisplayNodeAssertMainThread(); +} + +- (void)displayDidFinish +{ +} + +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale +{ + if (contentsScale != self.contentsScaleForDisplay) { + self.contentsScaleForDisplay = contentsScale; + [self setNeedsDisplay]; + } +} + +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale +{ + [self setNeedsDisplayAtScale:contentsScale]; + + ASDN::MutexLocker l(_propertyLock); + for (ASDisplayNode *child in _subnodes) { + [child recursivelySetNeedsDisplayAtScale:contentsScale]; + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_view) + return; + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = _view.superview; + [superview touchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_view) + return; + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = _view.superview; + [superview touchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_view) + return; + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = _view.superview; + [superview touchesEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + if (!_view) + return; + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = _view.superview; + [superview touchesCancelled:touches withEvent:event]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // This method is only implemented on UIView on iOS 6+. + ASDisplayNodeAssertMainThread(); + + if (!_view) + return YES; + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = _view.superview; + return [superview gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + return [_view hitTest:point withEvent:event]; +} + +- (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + _hitTestSlop = hitTestSlop; +} + +- (UIEdgeInsets)hitTestSlop +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _hitTestSlop; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + UIEdgeInsets slop = self.hitTestSlop; + if (_view && UIEdgeInsetsEqualToEdgeInsets(slop, UIEdgeInsetsZero)) { + // Safer to use UIView's -pointInside:withEvent: if we can. + return [_view pointInside:point withEvent:event]; + } else { + return CGRectContainsPoint(UIEdgeInsetsInsetRect(self.bounds, slop), point); + } +} + +#pragma mark - Pending View State +- (_ASPendingState *)pendingViewState +{ + if (!_pendingViewState) { + _pendingViewState = [[_ASPendingState alloc] init]; + ASDisplayNodeAssertNotNil(_pendingViewState, @"should have created a pendingViewState"); + } + + return _pendingViewState; +} + +- (void)_applyPendingStateToViewOrLayer +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(self.isViewLoaded, @"must have a view or layer"); + + // If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values + // for the view/layer are still valid. + ASDN::MutexLocker l(_propertyLock); + + if (_flags.isLayerBacked) { + [_pendingViewState applyToLayer:_layer]; + } else { + [_pendingViewState applyToView:_view]; + } + + [_pendingViewState release]; + _pendingViewState = nil; + + // TODO: move this into real pending state + if (_flags.preventOrCancelDisplay) { + self.asyncLayer.displaySuspended = YES; + } + if (!_flags.displaysAsynchronously) { + self.asyncLayer.displaysAsynchronously = NO; + } +} + +// This method has proved helpful in a few rare scenarios, similar to a category extension on UIView, but assumes knowledge of _ASDisplayView. +// It's considered private API for now and its use should not be encouraged. +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass +{ + ASDisplayNode *supernode = self.supernode; + while (supernode) { + if ([supernode isKindOfClass:supernodeClass]) + return supernode; + supernode = supernode.supernode; + } + + UIView *view = self.view.superview; + while (view) { + ASDisplayNode *viewNode = ((_ASDisplayView *)view).asyncdisplaykit_node; + if (viewNode) { + if ([viewNode isKindOfClass:supernodeClass]) + return viewNode; + } + + view = view.superview; + } + + return nil; +} + +- (void)recursiveSetPreventOrCancelDisplay:(BOOL)flag +{ + _recursiveSetPreventOrCancelDisplay(self, nil, flag); +} + +static void _recursiveSetPreventOrCancelDisplay(ASDisplayNode *node, CALayer *layer, BOOL flag) +{ + // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. + if (!layer && node && node.isViewLoaded) { + layer = node.layer; + } + + // If we don't know the node, but the layer is an async layer, get the node from the layer. + if (!node && layer && [layer isKindOfClass:[_ASDisplayLayer class]]) { + node = layer.asyncdisplaykit_node; + } + + // Set the flag on the node. If this is a pure layer (no node) then this has no effect (plain layers don't support preventing/cancelling display). + node.preventOrCancelDisplay = flag; + + if (layer) { + // If there is a layer, recurse down the layer hierarchy to set the flag on descendants. This will cover both layer-based and node-based children. + for (CALayer *sublayer in layer.sublayers) { + _recursiveSetPreventOrCancelDisplay(nil, sublayer, flag); + } + } else { + // If there is no layer (view not loaded yet), recurse down the subnode hierarchy to set the flag on descendants. This covers only node-based children, but for a node whose view is not loaded it can't possibly have nodeless children. + for (ASDisplayNode *subnode in node.subnodes) { + _recursiveSetPreventOrCancelDisplay(subnode, nil, flag); + } + } +} + +- (BOOL)preventOrCancelDisplay +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _flags.preventOrCancelDisplay; +} + +- (void)setPreventOrCancelDisplay:(BOOL)flag +{ + ASDisplayNodeAssertThreadAffinity(self); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (_flags.isSynchronous) + return; + + ASDN::MutexLocker l(_propertyLock); + + if (_flags.preventOrCancelDisplay == flag) + return; + + _flags.preventOrCancelDisplay = flag; + + self.asyncLayer.displaySuspended = flag; +} + +- (BOOL)isInWindow +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDN::MutexLocker l(_propertyLock); + return _flags.inWindow; +} + +- (void)setInWindow:(BOOL)inWindow +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDN::MutexLocker l(_propertyLock); + _flags.inWindow = inWindow; +} + ++ (dispatch_queue_t)asyncSizingQueue +{ + static dispatch_queue_t asyncSizingQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + asyncSizingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDisplayNode.asyncSizingQueue", DISPATCH_QUEUE_CONCURRENT); + // we use the highpri queue to prioritize UI rendering over other async operations + dispatch_set_target_queue(asyncSizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + }); + + return asyncSizingQueue; +} + +- (BOOL)_isMarkedForReplacement +{ + ASDN::MutexLocker l(_propertyLock); + + return _replaceAsyncSentinel != nil; +} + +- (ASSentinel *)_asyncReplaceSentinel +{ + ASDN::MutexLocker l(_propertyLock); + + if (!_replaceAsyncSentinel) { + _replaceAsyncSentinel = [[ASSentinel alloc] init]; + } + return [[_replaceAsyncSentinel retain] autorelease]; +} + +// Calls completion with nil to indicated cancellation +- (void)_enqueueAsyncSizingWithSentinel:(ASSentinel *)sentinel completion:(void(^)(ASDisplayNode *n))completion; +{ + int32_t sentinelValue = sentinel.value; + + // This is what we're going to use for sizing. Hope you like it :D + CGRect bounds = self.bounds; + + dispatch_async([[self class] asyncSizingQueue], ^{ + // Check sentinel before, bail early + if (sentinel.value != sentinelValue) + return dispatch_async(dispatch_get_main_queue(), ^{ completion(nil); }); + + [self sizeToFit:bounds.size]; + + // Check sentinel after, bail early + if (sentinel.value != sentinelValue) + return dispatch_async(dispatch_get_main_queue(), ^{ completion(nil); }); + + // Success; not cancelled + dispatch_async(dispatch_get_main_queue(), ^{ + completion(self); + }); + }); + +} + +- (void)replaceSubnodeAsynchronously:(ASDisplayNode *)old withNode:(ASDisplayNode *)replacement completion:(void(^)(BOOL cancelled, ASDisplayNode *replacement, ASDisplayNode *oldSubnode))completion +{ + + ASDisplayNodeAssert(old.supernode == self, @"Must replace something that is actually a subnode. You passed: %@", old); + ASDisplayNodeAssert(!replacement.isViewLoaded, @"Can't async size something that already has a view, since we currently have no way to convert a viewed node into a viewless one..."); + + // If we're already marked for replacement, cancel the pending request + ASSentinel *sentinel = [old _asyncReplaceSentinel]; + uint32_t sentinelValue = [sentinel increment]; + + // Enqueue async sizing on our argument + [replacement _enqueueAsyncSizingWithSentinel:sentinel completion:^(ASDisplayNode *replacementCompletedNode) { + // Sizing is done; swap with our other view + // Check sentinel one more time in case it changed during sizing + if (replacementCompletedNode && sentinel.value == sentinelValue) { + if (old.supernode) { + if (old.supernode.inWindow) { + // Now wait for async display before notifying delegate that replacement is complete + + // When async sizing is complete, add subnode below placeholder with 0 alpha + CGFloat replacementAlpha = replacement.alpha; + BOOL wasAsyncTransactionContainer = replacement.asyncdisplaykit_asyncTransactionContainer; + [old.supernode insertSubnode:replacement belowSubnode:old]; + + replacementCompletedNode.alpha = 0.0; + replacementCompletedNode.asyncdisplaykit_asyncTransactionContainer = YES; + + ASDisplayNodeCAssert(replacementCompletedNode.isViewLoaded, @".layer shouldn't be the thing to load the view"); + + [replacement.layer.asyncdisplaykit_asyncTransaction addCompletionBlock:^(id unused, BOOL canceled) { + ASDisplayNodeCAssertMainThread(); + + canceled |= (sentinel.value != sentinelValue); + + replacementCompletedNode.alpha = replacementAlpha; + replacementCompletedNode.asyncdisplaykit_asyncTransactionContainer = wasAsyncTransactionContainer; + + if (!canceled) { + [old removeFromSupernode]; + } else { + [replacementCompletedNode removeFromSupernode]; + } + + completion(canceled, replacementCompletedNode, old); + }]; + } else { + // Not in window, don't wait for async display + [old.supernode replaceSubnode:old withSubnode:replacement]; + completion(NO, replacementCompletedNode, old); + } + + } else { // Old has been moved no longer to be in the hierarchy + // TODO: add code to removeFromSupernode and hook UIView methods to cancel sentinel here? + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Tried to replaceSubnodeAsynchronously an ASDisplayNode, but then removed it from the hierarchy... what did you mean?" userInfo:nil]; + + completion(NO, replacementCompletedNode, old); + } + } else { // If we were cancelled + completion(YES, nil, nil); + } + }]; + + +} + +- (ASDisplayNode *)addSubnodeAsynchronously:(ASDisplayNode *)replacement completion:(void(^)(ASDisplayNode *fullySizedSubnode))completion +{ + ASDisplayNodeAssertThreadAffinity(self); + + // Create a placeholder ASDisplayNode that saves this guy's place in the view hierarchy for when things return later + ASDisplayNode *placeholder = [[ASDisplayNode alloc] init]; + + [self addSubnode:placeholder]; + [self replaceSubnodeAsynchronously:placeholder withNode:replacement completion:^(BOOL cancelled, ASDisplayNode *replacementBlock, ASDisplayNode *placeholderBlock) { + if (replacementBlock && placeholderBlock && !cancelled) { + [placeholderBlock removeFromSupernode]; + completion(replacementBlock); + } else { + [placeholderBlock removeFromSupernode]; + } + }]; + + return [placeholder autorelease]; +} + +@end + +@implementation ASDisplayNode (Debugging) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" +- (NSString *)description +{ + if (self.name) { + return [NSString stringWithFormat:@"<%@ %p name = %@>", self.class, self, self.name]; + } else { + return [super description]; + } +} +#pragma clang diagnostic pop + +- (NSString *)debugDescription +{ + NSString *notableTargetDesc = (_flags.isLayerBacked ? @" [layer]" : @" [view]"); + if (_view && _viewClass) { // Nonstandard view is loaded + notableTargetDesc = [NSString stringWithFormat:@" [%@ : %p]", _view.class, _view]; + } else if (_layer && _layerClass) { // Nonstandard layer is loaded + notableTargetDesc = [NSString stringWithFormat:@" [%@ : %p]", _layer.class, _layer]; + } else if (_viewClass) { // Nonstandard view class unloaded + notableTargetDesc = [NSString stringWithFormat:@" [%@]", _viewClass]; + } else if (_layerClass) { // Nonstandard layer class unloaded + notableTargetDesc = [NSString stringWithFormat:@" [%@]", _layerClass]; + } + if (self.name) { + return [NSString stringWithFormat:@"<%@ %p name = %@%@>", self.class, self, self.name, notableTargetDesc]; + } else { + return [NSString stringWithFormat:@"<%@ %p%@>", self.class, self, notableTargetDesc]; + } +} + +- (NSString *)descriptionForRecursiveDescription +{ + NSString *creationTypeString = nil; +#if TIME_DISPLAYNODE_OPS + creationTypeString = [NSString stringWithFormat:@"cr8:%.2lfms dl:%.2lfms ap:%.2lfms ad:%.2lfms", 1000 * _debugTimeToCreateView, 1000 * _debugTimeForDidLoad, 1000 * _debugTimeToApplyPendingState, 1000 * _debugTimeToAddSubnodeViews]; +#endif + + return [NSString stringWithFormat:@"<%@ alpha:%.2f isLayerBacked:%d %@>", self.description, self.alpha, self.isLayerBacked, creationTypeString]; +} + +- (NSString *)displayNodeRecursiveDescription +{ + return [self _recursiveDescriptionHelperWithIndent:@""]; +} + +- (NSString *)_recursiveDescriptionHelperWithIndent:(NSString *)indent +{ + NSMutableString *subtree = [[[[indent stringByAppendingString: self.descriptionForRecursiveDescription] stringByAppendingString:@"\n"] mutableCopy] autorelease]; + for (ASDisplayNode *n in self.subnodes) { + [subtree appendString:[n _recursiveDescriptionHelperWithIndent:[indent stringByAppendingString:@" | "]]]; + } + return subtree; +} + +@end + +// We use associated objects as a last resort if our view is not a _ASDisplayView ie it doesn't have the _node ivar to write to + +static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; + +@implementation UIView (ASDisplayNodeInternal) +@dynamic asyncdisplaykit_node; + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, node, OBJC_ASSOCIATION_ASSIGN); // Weak reference to avoid cycle, since the node retains the view. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASDisplayNode *node = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return node; +} + +@end + +@implementation CALayer (ASDisplayNodeInternal) +@dynamic asyncdisplaykit_node; +@end diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h new file mode 100644 index 0000000000..ed42c950be --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -0,0 +1,68 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import "ASBaseDefines.h" +#import "ASDisplayNode.h" + +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + Given a layer, returns the associated display node, if any. + */ +extern ASDisplayNode *ASLayerToDisplayNode(CALayer *layer); + +/** + Given a view, returns the associated display node, if any. + */ +extern ASDisplayNode *ASViewToDisplayNode(UIView *view); + +/** + Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. + */ +extern id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. + */ +extern id ASDisplayNodeFindClass(ASDisplayNode *start, Class c); + +/** + Given a display node, collects all descendents. This is a specialization of ASCollectContainer() that walks the Core Animation layer tree as opposed to the display node tree, thus supporting non-continues display node hierarchies. + */ +extern NSArray *ASCollectDisplayNodes(ASDisplayNode *node); + +/** + Given a display node, traverses down the node hierarchy, returning all the display nodes that pass the block. + */ +extern NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses down the node hierarchy, returning all the display nodes of kind class. + */ +extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c); + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node that pass the block. + */ +extern id ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node of kind class. + */ +extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c); + +/** + Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. + */ +extern void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node); +extern void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm new file mode 100644 index 0000000000..4849de7414 --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -0,0 +1,133 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDisplayNodeExtras.h" + +#import "ASDisplayNodeInternal.h" + +ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) +{ + return layer.asyncdisplaykit_node; +} + +ASDisplayNode *ASViewToDisplayNode(UIView *view) +{ + return view.asyncdisplaykit_node; +} + +id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +{ + CALayer *layer = node.layer; + + while (layer) { + node = ASLayerToDisplayNode(layer); + if (block(node)) { + return node; + } + layer = layer.superlayer; + } + + return nil; +} + +id ASDisplayNodeFindClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFind(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +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); +} + +extern NSArray *ASCollectDisplayNodes(ASDisplayNode *node) +{ + NSMutableArray *list = [NSMutableArray array]; + 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:node]; + } + + _ASDisplayNodeFindAllSubnodes(array, subnode, block); + } +} + +extern NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) +{ + NSMutableArray *list = [NSMutableArray array]; + _ASDisplayNodeFindAllSubnodes(list, start, block); + return list; +} + +extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindAllSubnodes(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +#pragma mark - Find first subnode + +static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in startNode.subnodes) { + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstSubnode(subnode, YES, block); + if (foundNode) { + return foundNode; + } + } + + if (includeStartNode && block(startNode)) + return startNode; + + return nil; +} + +extern id ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstSubnode(startNode, NO, block); +} + +extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindFirstSubnode(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node) +{ + [node __incrementVisibilityNotificationsDisabled]; +} + +void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node) +{ + [node __decrementVisibilityNotificationsDisabled]; +} diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h new file mode 100644 index 0000000000..c0cbc6d7c6 --- /dev/null +++ b/AsyncDisplayKit/ASImageNode.h @@ -0,0 +1,53 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef NS_ENUM(NSUInteger, ASImageNodeTint) { + ASImageNodeTintNormal = 0, + ASImageNodeTintGreyscale, +}; +// FIXME: This class should not derive from ASControlNode once ASButtonNode is implemented. +@interface ASImageNode : ASControlNode + +@property (atomic, retain) UIImage *image; // The node will efficiently display stretchable images by using the layer's contentsCenter property. + // Non-stretchable images work too, of course. + +// This is a simple API if you want to tint the image +@property (nonatomic, assign) ASImageNodeTint tint; + +#pragma mark - Cropping +//! @abstract Indicates whether efficient cropping of the receiver is enabled. Defaults to YES. See -setCropEnabled:recropImmediately:inBounds: for more information. +@property (nonatomic, assign) BOOL cropEnabled; + +/** + @abstract Enables or disables efficient cropping. + @param cropEnabled YES to efficiently crop the receiver's contents such that contents outside of its bounds are not included; NO otherwise. + @param recropImmediately If the receiver has an image, YES to redisplay the receiver immediately; NO otherwise. + @param cropBounds The bounds into which the receiver will be cropped. Useful if bounds are to change in response to cropping (but have not yet done so). + @discussion Efficient cropping is only performed when the receiver's view's contentMode is UIViewContentModeScaleAspectFill. By default, cropping is enabled. The crop alignment may be controlled via cropAlignmentFactor. + */ +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds; + +/** + @abstract A value that controls how the receiver's efficient cropping is aligned. + @discussion This value defines a rectangle that is to be featured by the receiver. The rectangle is specified as a "unit rectangle," using percentages of the source image's width and height, e.g. CGRectMake(0.5, 0, 0.5, 1.0) will feature the full right half a photo. If the cropRect is empty, the content mode of the receiver will be used to determine its dimensions, and only the cropRect's origin will be used for positioning. The default value of this property is CGRectMake(0.5, 0.5, 0.0, 0.0). + */ +@property (nonatomic, readwrite, assign) CGRect cropRect; + + +#pragma mark - +/** + @abstract Marks the receiver as needing display and performs a block after display has finished. + @param displayCompletionBlock The block to be performed after display has finished. + @param canceled YES if display was prevented or canceled (via preventOrCancelDisplay); NO otherwise. + @discussion displayCompletionBlock will be performed on the main-thread. If `preventOrCancelDisplay` is YES, `displayCompletionBlock` is will be performed immediately and `YES` will be passed for `canceled`. + */ +- (void)setNeedsDisplayWithCompletion:(void (^)(BOOL canceled))displayCompletionBlock; + +@end diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm new file mode 100644 index 0000000000..0970706a2b --- /dev/null +++ b/AsyncDisplayKit/ASImageNode.mm @@ -0,0 +1,362 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASImageNode.h" + +#import +#import +#import +#import + +#import "ASImageNode+CGExtras.h" + +@interface _ASImageNodeDrawParameters : NSObject + +@property (nonatomic, assign, readonly) BOOL cropEnabled; +@property (nonatomic, assign) BOOL opaque; +@property (nonatomic, retain) UIImage *image; +@property (nonatomic, assign) CGRect bounds; +@property (nonatomic, assign) CGFloat contentsScale; +@property (nonatomic, assign) ASImageNodeTint tint; +@property (nonatomic, retain) UIColor *backgroundColor; +@property (nonatomic, assign) UIViewContentMode contentMode; +@property (nonatomic, assign) CGRect cropRect; + +@end + +// TODO: eliminate explicit parameters with a set of keys copied from the node +@implementation _ASImageNodeDrawParameters + +- (id)initWithCrop:(BOOL)cropEnabled opaque:(BOOL)opaque image:(UIImage *)image bounds:(CGRect)bounds contentsScale:(CGFloat)contentsScale backgroundColor:(UIColor *)backgroundColor tint:(ASImageNodeTint)tint contentMode:(UIViewContentMode)contentMode cropRect:(CGRect)cropRect +{ + self = [self init]; + if (!self) return nil; + + _cropEnabled = cropEnabled; + _opaque = opaque; + _image = [image retain]; + _bounds = bounds; + _contentsScale = contentsScale; + _backgroundColor = [backgroundColor retain]; + _tint = tint; + _contentMode = contentMode; + _cropRect = cropRect; + + return self; +} + +- (void)dealloc +{ + [_image release]; + [_backgroundColor release]; + [super dealloc]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ : %p image:%@ cropEnabled:%@ opaque:%@ bounds:%@ contentsScale:%.2f backgroundColor:%@ tint:%u contentMode:%@ cropRect:%@>", [self class], self, self.image, @(self.cropEnabled), @(self.opaque), NSStringFromCGRect(self.bounds), self.contentsScale, self.backgroundColor, self.tint, ASDisplayNodeNSStringFromUIContentMode(self.contentMode), NSStringFromCGRect(self.cropRect)]; +} + +@end + + +@implementation ASImageNode +{ +@private + UIImage *_image; + + void (^_displayCompletionBlock)(BOOL canceled); + ASDN::RecursiveMutex _imageLock; + + // Cropping. + BOOL _cropEnabled; // Defaults to YES. + ASImageNodeTint _tint; + CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) + CGRect _cropDisplayBounds; +} + +@synthesize image = _image; + +- (id)init +{ + if (!(self = [super init])) + return nil; + + // TODO can this be removed? + self.contentsScale = [[UIScreen mainScreen] scale]; + self.contentMode = UIViewContentModeScaleAspectFill; + self.opaque = YES; + + _cropEnabled = YES; + _cropRect = CGRectMake(0.5, 0.5, 0, 0); + _cropDisplayBounds = CGRectNull; + + return self; +} + +- (void)dealloc +{ + [_displayCompletionBlock release]; + [_image release]; + [super dealloc]; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDN::MutexLocker l(_imageLock); + if (_image) + return _image.size; + else + return CGSizeZero; +} + +- (void)setImage:(UIImage *)image +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_imageLock); + if (_image != image) { + [_image release]; + _image = [image retain]; + [self invalidateCalculatedSize]; + [self setNeedsDisplay]; + } +} + +- (UIImage *)image +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_imageLock); + return [[_image retain] autorelease]; +} + +- (void)setTint:(ASImageNodeTint)tint +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_imageLock); + if (_tint != tint) { + _tint = tint; + [self setNeedsDisplay]; + } +} + +- (ASImageNodeTint)tint +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_imageLock); + return _tint; +} + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; +{ + BOOL hasValidCropBounds = _cropEnabled && !CGRectIsNull(_cropDisplayBounds) && !CGRectIsEmpty(_cropDisplayBounds); + + return [[[_ASImageNodeDrawParameters alloc] initWithCrop:_cropEnabled + opaque:self.opaque + image:self.image + bounds:(hasValidCropBounds ? _cropDisplayBounds : self.bounds) + contentsScale:self.contentsScaleForDisplay + backgroundColor:self.backgroundColor + tint:self.tint + contentMode:self.contentMode + cropRect:self.cropRect] autorelease]; +} + ++ (UIImage *)displayWithParameters:(_ASImageNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + UIImage *image = parameters.image; + + if (!image) { + return nil; + } + + ASDisplayNodeAssert(parameters.contentsScale > 0, @"invalid contentsScale at display time"); + + CGRect bounds = parameters.bounds; + + CGFloat contentsScale = parameters.contentsScale; + UIViewContentMode contentMode = parameters.contentMode; + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(floorf(bounds.size.width * contentsScale), floorf(bounds.size.height * contentsScale)); + + CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image.CGImage); + BOOL imageHasAlpha = alphaInfo == kCGImageAlphaFirst + || alphaInfo == kCGImageAlphaLast + || alphaInfo == kCGImageAlphaPremultipliedFirst + || alphaInfo == kCGImageAlphaPremultipliedLast; + + BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill + || contentMode == UIViewContentModeScaleAspectFit; + + CGSize backingSize; + CGRect imageDrawRect; + + if (boundsSizeInPixels.width * contentsScale < 1.0f || + boundsSizeInPixels.height * contentsScale < 1.0f || + imageSizeInPixels.width < 1.0f || + imageSizeInPixels.height < 1.0f) { + return nil; + } + + // If we're not supposed to do any cropping, just decode image at original size + if (!parameters.cropEnabled || !contentModeSupported || stretchable) { + backingSize = imageSizeInPixels; + imageDrawRect = (CGRect){.size = backingSize}; + } else { + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels, + boundsSizeInPixels, + contentMode, + parameters.cropRect, + &backingSize, + &imageDrawRect); + } + + if (backingSize.width <= 0.0f || + backingSize.height <= 0.0f || + imageDrawRect.size.width <= 0.0f || + imageDrawRect.size.height <= 0.0f) { + return nil; + } + + // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds + // will do its rounding on pixel instead of point boundaries + UIGraphicsBeginImageContextWithOptions(backingSize, !imageHasAlpha, 1.0); + + CGContextRef context = UIGraphicsGetCurrentContext(); + + [image drawInRect:imageDrawRect]; + + if (parameters.tint == ASImageNodeTintGreyscale) { + [[UIColor grayColor] setFill]; + CGContextSetBlendMode(context, kCGBlendModeColor); + CGContextFillRect(context, (CGRect){.size = backingSize}); + } + + if (isCancelled()) { + UIGraphicsEndImageContext(); + return nil; + } + + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + + UIGraphicsEndImageContext(); + + if (stretchable) { + return [image resizableImageWithCapInsets:image.capInsets resizingMode:image.resizingMode]; + } + + return result; +} + +- (void)didDisappear +{ + self.contents = nil; + [super didDisappear]; +} + +- (void)willAppear +{ + [super willAppear]; + + if (!self.layer.contents) + [self setNeedsDisplay]; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + // If we've got a block to perform after displaying, do it. + if (self.image && _displayCompletionBlock) { + + // FIXME: _displayCompletionBlock is not protected by lock + _displayCompletionBlock(NO); + [_displayCompletionBlock release]; + _displayCompletionBlock = nil; + } +} + +#pragma mark - +- (void)setNeedsDisplayWithCompletion:(void (^)(BOOL canceled))displayCompletionBlock +{ + if (self.preventOrCancelDisplay) { + if (displayCompletionBlock) + displayCompletionBlock(YES); + return; + } + + // Stash the block and call-site queue. We'll invoke it in -displayDidFinish. + // FIXME: _displayCompletionBlock not protected by lock + if (_displayCompletionBlock != displayCompletionBlock) { + [_displayCompletionBlock release]; + _displayCompletionBlock = [displayCompletionBlock copy]; + } + + [self setNeedsDisplay]; +} + +#pragma mark - Cropping +- (BOOL)cropEnabled +{ + ASDisplayNodeAssertThreadAffinity(self); + return _cropEnabled; +} + +- (void)setCropEnabled:(BOOL)cropEnabled +{ + ASDisplayNodeAssertThreadAffinity(self); + [self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds]; +} + +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds +{ + ASDisplayNodeAssertThreadAffinity(self); + if (_cropEnabled == cropEnabled) + return; + + _cropEnabled = cropEnabled; + _cropDisplayBounds = cropBounds; + + // If we have an image to display, display it, respecting our recrop flag. + if (self.image) + { + if (recropImmediately) + [self displayImmediately]; + else + [self setNeedsDisplay]; + } +} + +- (CGRect)cropRect +{ + ASDisplayNodeAssertThreadAffinity(self); + return _cropRect; +} + +- (void)setCropRect:(CGRect)cropRect +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (CGRectEqualToRect(_cropRect, cropRect)) + return; + + _cropRect = cropRect; + + // TODO: this logic needs to be updated to respect cropRect. + CGSize boundsSize = self.bounds.size; + CGSize imageSize = self.image.size; + + BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height)); + + // Re-display if we need to. + if (self.isViewLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage) + [self setNeedsDisplay]; +} + +@end diff --git a/AsyncDisplayKit/ASTextNode.h b/AsyncDisplayKit/ASTextNode.h new file mode 100644 index 0000000000..84dd391eaa --- /dev/null +++ b/AsyncDisplayKit/ASTextNode.h @@ -0,0 +1,203 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@protocol ASTextNodeDelegate; + +typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { + ASTextNodeHighlightStyleLight, + ASTextNodeHighlightStyleDark +}; + +/** + @abstract Draws interactive rich text. + @discussion Backed by TextKit. + */ +@interface ASTextNode : ASControlNode + +/** + @abstract The attributed string to show. + @discussion Defaults to nil, no text is shown. + For inline image attachments, add an attribute of key NSAttachmentAttributeName, with a value of an NSTextAttachment. + */ +@property (nonatomic, copy) NSAttributedString *attributedString; + +#pragma mark - Truncation + +/** + @abstract The attributedString to use when the text must be truncated. + @discussion Defaults to a localized ellipsis character. + */ +@property (nonatomic, copy) NSAttributedString *truncationAttributedString; + +/** + @summary The second attributed string appended for truncation. + @discussion This string will be highlighted on touches. + @default nil + */ +@property (nonatomic, copy) NSAttributedString *additionalTruncationMessage; + +/** + @abstract Determines how the text is truncated to fit within the receiver's maximum size. + @discussion Defaults to NSLineBreakByWordWrapping. + */ +@property (nonatomic, assign) NSLineBreakMode truncationMode; + +/** + @abstract If the text node is truncated. Text must have been sized first. + */ +@property (nonatomic, assign, readonly, getter = isTruncated) BOOL truncated; + +/** + @abstract The number of lines in the text. Text must have been sized first. + */ +@property (nonatomic, assign, readonly) NSUInteger lineCount; + +#pragma mark - Shadow + +/** + @abstract When you set these ASDisplayNode properties, they are composited into the bitmap instead of being applied by CA. + + @property (atomic, assign) CGColorRef shadowColor; + @property (atomic, assign) CGFloat shadowOpacity; + @property (atomic, assign) CGSize shadowOffset; + @property (atomic, assign) CGFloat shadowRadius; + */ + +/** + @abstract The number of pixels used for shadow padding on each side of the receiver. + @discussion Each inset will be less than or equal to zero, so that applying + UIEdgeInsetsRect(boundingRectForText, shadowPadding) + will return a CGRect large enough to fit both the text and the appropriate shadow padding. + */ +@property (nonatomic, readonly, assign) UIEdgeInsets shadowPadding; + +#pragma mark - Positioning + +/** + @abstract Returns an array of rects bounding the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange; + +/** + @abstract Returns an array of rects used for highlighting the characters in a given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion Use this method to detect all the different rectangles the highlights of a given range of text occupies. + The rects returned are not guaranteed to be contiguous (for example, if the given text range spans + a line break, the rects returned will be on opposite sides and different lines). The rects returned + are in the coordinate system of the receiver. This method is useful for visual coordination with a + highlighted range of text. + */ +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange; + +/** + @abstract Returns a bounding rect for the given text range. + @param textRange A range of text. Must be valid for the receiver's string. + @discussion The height of the frame returned is that of the receiver's line-height; adjustment for + cap-height and descenders is not performed. This method raises an exception if textRange is not + a valid substring range of the receiver's string. + */ +- (CGRect)frameForTextRange:(NSRange)textRange; + +/** + @abstract Returns the trailing rectangle of space in the receiver, after the final character. + @discussion Use this method to detect which portion of the receiver is not occupied by characters. + The rect returned is in the coordinate system of the receiver. + */ +- (CGRect)trailingRect; + + +#pragma mark - Actions + +/** + @abstract The set of attribute names to consider links. + */ +@property (nonatomic, copy) NSArray *linkAttributeNames; + +/** + @abstract Indicates whether the receiver has an entity at a given point. + @param point The point, in the receiver's coordinate system. + @param attributeNameOut The name of the attribute at the point. Can be NULL. + @param rangeOut The ultimate range of the found text. Can be NULL. + @result YES if an entity exists at `point`; NO otherwise. + */ +- (id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString **)attributeNameOut range:(out NSRange *)rangeOut; + +/** + @abstract The style to use when highlighting text. + */ +@property (nonatomic, assign) ASTextNodeHighlightStyle highlightStyle; + +/** + @abstract The range of text highlighted by the receiver. Changes to this property are not animated by default. + */ +@property (nonatomic, assign) NSRange highlightRange; + +/** + @abstract Set the range of text to highlight, with optional animation. + */ +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated; + +/** + @abstract Responds to actions from links in the text node. + */ +@property (nonatomic, weak) id delegate; + +@end + +@protocol ASTextNodeDelegate +@optional + +/** + @abstract Indicates to the delegate that a link was tapped within a rich text node. + @param richTextNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within richTextNode, in richTextNode's coordinate system, that was tapped. + */ +- (void)richTextNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +/** + @abstract Indicates to the delegate that a link was tapped within a rich text node. + @param richTextNode The ASTextNode containing the link that was tapped. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @param point The point within richTextNode, in richTextNode's coordinate system, that was tapped. + */ +- (void)richTextNode:(ASTextNode *)richTextNode longPressedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange; + +//! @abstract Called when the rich text node's truncation string has been tapped. +- (void)richTextNodeTappedTruncationToken:(ASTextNode *)richTextNode; + +/** + @abstract Indicates to the rich text node if an attribute should be considered a link. + @param richTextNode The rich text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @discussion If not implemented, the default value is NO. + @return YES if the entity attribute should be a link, NO otherwise. + */ +- (BOOL)richTextNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value; + +/** + @abstract Indicates to the rich text node if an attribute is a valid long-press target + @param richTextNode The rich text node containing the entity attribute. + @param attribute The attribute that was tapped. Will not be nil. + @param value The value of the tapped attribute. + @discussion If not implemented, the default value is NO. + @return YES if the entity attribute should be treated as a long-press target, NO otherwise. + */ +- (BOOL)richTextNode:(ASTextNode *)richTextNode shouldLongPressLinkAttribute:(NSString *)attribute value:(id)value; + +@end diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm new file mode 100644 index 0000000000..6640a6302b --- /dev/null +++ b/AsyncDisplayKit/ASTextNode.mm @@ -0,0 +1,940 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNode.h" + +#import +#import +#import +#import +#import +#import + +#import "ASTextNodeRenderer.h" +#import "ASTextNodeShadower.h" + +static const NSTimeInterval ASTextNodeHighlightFadeOutDuration = 0.15; +static const NSTimeInterval ASTextNodeHighlightFadeInDuration = 0.1; +static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; +static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; +static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; + +@interface ASTextNodeDrawParameters : NSObject + +- (instancetype)initWithRenderer:(ASTextNodeRenderer *)renderer + shadower:(ASTextNodeShadower *)shadower + textOrigin:(CGPoint)textOrigin + backgroundColor:(CGColorRef)backgroundColor; + +@property (nonatomic, strong, readonly) ASTextNodeRenderer *renderer; + +@property (nonatomic, strong, readonly) ASTextNodeShadower *shadower; + +@property (nonatomic, assign, readonly) CGPoint textOrigin; + +@property (nonatomic, assign, readonly) CGColorRef backgroundColor; + +@end + +@implementation ASTextNodeDrawParameters + +- (instancetype)initWithRenderer:(ASTextNodeRenderer *)renderer + shadower:(ASTextNodeShadower *)shadower + textOrigin:(CGPoint)textOrigin + backgroundColor:(CGColorRef)backgroundColor +{ + if (self = [super init]) { + _renderer = renderer; + _shadower = shadower; + _textOrigin = textOrigin; + _backgroundColor = CGColorRetain(backgroundColor); + } + return self; +} + +- (void)dealloc +{ + CGColorRelease(_backgroundColor); +} + +@end + +ASDISPLAYNODE_INLINE CGFloat ceilPixelValueForScale(CGFloat f, CGFloat scale) +{ + // Round up to device pixel (.5 on retina) + return ceilf(f * scale) / scale; +} + +ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) +{ + return ceilPixelValueForScale(f, [UIScreen mainScreen].scale); +} + + +@interface ASTextNode () + +@end + +@implementation ASTextNode { + CGSize _shadowOffset; + CGColorRef _shadowColor; + CGFloat _shadowOpacity; + CGFloat _shadowRadius; + + NSAttributedString *_composedTruncationString; + + NSString *_highlightedLinkAttributeName; + id _highlightedLinkAttributeValue; + NSRange _highlightRange; + ASHighlightOverlayLayer *_activeHighlightLayer; + + ASDN::Mutex _rendererLock; + + CGSize _constrainedSize; + + ASTextNodeRenderer *_renderer; + ASTextNodeShadower *_shadower; + + UILongPressGestureRecognizer *_longPressGestureRecognizer; +} + +#pragma mark - NSObject + +- (instancetype)init +{ + if (self = [super init]) { + // Load default values from superclass. + _shadowOffset = [super shadowOffset]; + CGColorRef superColor = [super shadowColor]; + if (superColor != NULL) { + _shadowColor = CGColorRetain(superColor); + } + _shadowOpacity = [super shadowOpacity]; + _shadowRadius = [super shadowRadius]; + + // Enable user interaction for text node, as ASControlNode disables it by default. + self.userInteractionEnabled = YES; + self.needsDisplayOnBoundsChange = YES; + + _truncationMode = NSLineBreakByWordWrapping; + _truncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; + + // The common case is for a text node to be non-opaque and blended over some background. + self.opaque = NO; + + // Accessibility + self.isAccessibilityElement = YES; + self.accessibilityTraits = UIAccessibilityTraitStaticText; + + _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); + } + + return self; +} + +- (void)dealloc +{ + if (_shadowColor != NULL) { + CGColorRelease(_shadowColor); + } + + if (_longPressGestureRecognizer) { + _longPressGestureRecognizer.delegate = nil; + [_longPressGestureRecognizer removeTarget:nil action:NULL]; + [self.view removeGestureRecognizer:_longPressGestureRecognizer]; + } +} + +- (NSString *)description +{ + NSString *plainString = [[_attributedString string] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + NSString *truncationString = [_composedTruncationString string]; + if (plainString.length > 50) + plainString = [[plainString substringToIndex:50] stringByAppendingString:@"\u2026"]; + return [NSString stringWithFormat:@"<%@: %p; text = \"%@\"; truncation string = \"%@\"; frame = %@>", self.class, self, plainString, truncationString, self.isViewLoaded ? NSStringFromCGRect(self.layer.frame) : nil]; +} + +#pragma mark - ASDisplayNode + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); + ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); + // The supplied constrainedSize should include room for shadowPadding. + // Inset the constrainedSize by the shadow padding to get the size available for text. + UIEdgeInsets shadowPadding = [[self _shadower] shadowPadding]; + // Invert the negative values of shadow padding to get a positive inset + UIEdgeInsets shadowPaddingOutset = ASDNEdgeInsetsInvert(shadowPadding); + + // Inset the padded constrainedSize to get the remaining size available for text + CGRect constrainedRect = CGRect{CGPointZero, constrainedSize}; + CGSize constrainedSizeForText = UIEdgeInsetsInsetRect(constrainedRect, shadowPaddingOutset).size; + ASDisplayNodeAssert(constrainedSizeForText.width >= 0, @"Constrained width for text (%f) after subtracting shadow padding (%@) is too narrow", constrainedSizeForText.width, NSStringFromUIEdgeInsets(shadowPadding)); + ASDisplayNodeAssert(constrainedSizeForText.height >= 0, @"Constrained height for text (%f) after subtracting shadow padding (%@) is too short", constrainedSizeForText.height, NSStringFromUIEdgeInsets(shadowPadding)); + + _constrainedSize = constrainedSizeForText; + [self _invalidateRenderer]; + [self setNeedsDisplay]; + CGSize rendererSize = [[self _renderer] size]; + + // Add shadow padding back + CGSize renderSizePlusShadowPadding = UIEdgeInsetsInsetRect(CGRect{CGPointZero, rendererSize}, shadowPadding).size; + ASDisplayNodeAssert(renderSizePlusShadowPadding.width >= 0, @"Calculated width for text with shadow padding (%f) is too narrow", constrainedSizeForText.width); + ASDisplayNodeAssert(renderSizePlusShadowPadding.height >= 0, @"Calculated height for text with shadow padding (%f) is too short", constrainedSizeForText.height); + + return CGSizeMake(fminf(ceilPixelValue(renderSizePlusShadowPadding.width), constrainedSize.width), + fminf(ceilPixelValue(renderSizePlusShadowPadding.height), constrainedSize.height)); +} + +- (void)willAppear +{ + CALayer *layer = self.layer; + if (!layer.contents) { + // This can happen on occasion that the layer will not display unless this + // set. + [layer setNeedsDisplay]; + } + [super willAppear]; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + // We invalidate our renderer here to clear the very high memory cost of + // keeping this around. _invalidateRenderer will dealloc this onto a bg + // thread resulting in less stutters on the main thread than if it were + // to be deallocated in dealloc. This is also helpful in opportunistically + // reducing memory consumption and reducing the overall footprint of the app. + [self _invalidateRenderer]; +} + +- (void)didDisappear +{ + // We nil out the contents and kill our renderer to prevent the very large + // memory overhead of maintaining these for all text nodes. They can be + // regenerated when layout is necessary. + self.contents = nil; + + [self _invalidateRenderer]; + + [super didDisappear]; +} + +- (void)didLoad +{ + [super didLoad]; + + // If we are view-backed, support gesture interaction. + if (!self.isLayerBacked) { + _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_handleLongPress:)]; + _longPressGestureRecognizer.delegate = self; + [self.view addGestureRecognizer:_longPressGestureRecognizer]; + } +} + +#pragma mark - Renderer Management + +- (ASTextNodeRenderer *)_renderer +{ + ASDN::MutexLocker l(_rendererLock); + if (_renderer == nil) { + CGSize constrainedSize = _constrainedSize.width != -INFINITY ? _constrainedSize : self.bounds.size; + _renderer = [[ASTextNodeRenderer alloc] initWithAttributedString:_attributedString + truncationString:_composedTruncationString + truncationMode:_truncationMode + constrainedSize:constrainedSize]; + } + return _renderer; +} + +- (void)_invalidateRenderer +{ + ASDN::MutexLocker l(_rendererLock); + if (_renderer) { + // Destruction of the layout managers/containers/text storage is quite + // expensive, and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + __block ASTextNodeRenderer *renderer = _renderer; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + renderer = nil; + }); + } + _renderer = nil; +} + +#pragma mark - Shadow Drawer Management +- (ASTextNodeShadower *)_shadower +{ + if (_shadower == nil) { + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:_shadowOffset + shadowColor:_shadowColor + shadowOpacity:_shadowOpacity + shadowRadius:_shadowRadius]; + } + return _shadower; +} + +- (void)_invalidateShadower +{ + _shadower = nil; +} + +#pragma mark - Modifying User Text + +- (void)setAttributedString:(NSAttributedString *)attributedString { + if (attributedString == _attributedString) { + return; + } + + if (attributedString == nil) { + attributedString = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + } + + _attributedString = ASCleanseAttributedStringOfCoreTextAttributes(attributedString); + + // We need an entirely new renderer + [self _invalidateRenderer]; + + // Tell the display node superclasses that the cached sizes are incorrect now + [self invalidateCalculatedSize]; + + [self setNeedsDisplay]; + + self.accessibilityLabel = _attributedString.string; + + if (_attributedString.length == 0) { + // We're not an accessibility element by default if there is no string. + self.isAccessibilityElement = NO; + } else { + self.isAccessibilityElement = YES; + } +} + +#pragma mark - Drawing + ++ (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + + // Fill background + if (!isRasterizing) { + CGColorRef backgroundColor = parameters.backgroundColor; + if (backgroundColor) { + CGContextSetFillColorWithColor(context, backgroundColor); + CGContextSetBlendMode(context, kCGBlendModeCopy); + // outset the background fill to cover fractional errors when drawing at a + // small contentsScale. + CGContextFillRect(context, CGRectInset(bounds, -2, -2)); + CGContextSetBlendMode(context, kCGBlendModeNormal); + } + } + + // Draw shadow + [parameters.shadower setShadowInContext:context]; + + // Draw text + bounds.origin = parameters.textOrigin; + [parameters.renderer drawInRect:bounds isRasterizing:isRasterizing]; + + CGContextRestoreGState(context); +} + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + // Offset the text origin by any shadow padding + UIEdgeInsets shadowPadding = [self shadowPadding]; + CGPoint textOrigin = CGPointMake(self.bounds.origin.x - shadowPadding.left, self.bounds.origin.y - shadowPadding.top); + return [[ASTextNodeDrawParameters alloc] initWithRenderer:[self _renderer] + shadower:[self _shadower] + textOrigin:textOrigin + backgroundColor:self.backgroundColor.CGColor]; +} + +#pragma mark - Attributes + +- (id)linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut +{ + return [self _linkAttributeValueAtPoint:point + attributeName:attributeNameOut + range:rangeOut + inAdditionalTruncationMessage:NULL]; +} + +- (id)_linkAttributeValueAtPoint:(CGPoint)point + attributeName:(out NSString **)attributeNameOut + range:(out NSRange *)rangeOut + inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut +{ + ASTextNodeRenderer *renderer = [self _renderer]; + NSRange visibleRange = [renderer visibleRange]; + NSAttributedString *attributedString = _attributedString; + + // Check in a 9-point region around the actual touch point so we make sure + // we get the best attribute for the touch. + __block CGFloat minimumGlyphDistance = CGFLOAT_MAX; + + // Final output vars + __block id linkAttributeValue = nil; + __block NSString *linkAttributeName = nil; + __block BOOL inTruncationMessage = NO; + + [renderer enumerateTextIndexesAtPosition:point usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) { + CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect)); + CGFloat currentDistance = sqrtf(powf(point.x - glyphLocation.x, 2.f) + powf(point.y - glyphLocation.y, 2.f)); + if (currentDistance >= minimumGlyphDistance) { + // If the distance computed from the touch to the glyph location is + // not the minimum among the located link attributes, we can just skip + // to the next location. + return; + } + + // Check if it's outside the visible range, if so, then we mark this touch + // as inside the truncation message, because in at least one of the touch + // points it was. + if (!(NSLocationInRange(characterIndex, visibleRange))) { + inTruncationMessage = YES; + } + + if (inAdditionalTruncationMessageOut != NULL) { + *inAdditionalTruncationMessageOut = inTruncationMessage; + } + + // Short circuit here if it's just in the truncation message. Since the + // truncation message may be beyond the scope of the actual input string, + // we have to make sure that we don't start asking for attributes on it. + if (inTruncationMessage) { + return; + } + + for (NSString *attributeName in _linkAttributeNames) { + NSRange range; + id value = [attributedString attribute:attributeName atIndex:characterIndex longestEffectiveRange:&range inRange:visibleRange]; + NSString *name = attributeName; + + if (value == nil || name == nil) { + // Didn't find anything + continue; + } + + // Check if delegate implements optional method, if not assume NO. + // Should the text be highlightable/touchable? + if (![_delegate respondsToSelector:@selector(richTextNode:shouldHighlightLinkAttribute:value:)] || + ![_delegate richTextNode:self shouldHighlightLinkAttribute:name value:value]) { + value = nil; + name = nil; + } + + if (value != nil || name != nil) { + // We found a minimum glyph distance link attribute, so set the min + // distance, and the out params. + minimumGlyphDistance = currentDistance; + + if (rangeOut != NULL && value != nil) { + *rangeOut = range; + // Limit to only the visible range, because the attributed string will + // return values outside the visible range. + if (NSMaxRange(*rangeOut) > NSMaxRange(visibleRange)) { + (*rangeOut).length = MAX(NSMaxRange(visibleRange) - (*rangeOut).location, 0); + } + } + + if (attributeNameOut != NULL) { + *attributeNameOut = name; + } + + // Set the values for the next iteration + linkAttributeValue = value; + linkAttributeName = name; + + break; + } + } + }]; + + return linkAttributeValue; +} + +#pragma mark - UIGestureRecognizerDelegate + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + if (gestureRecognizer == _longPressGestureRecognizer) { + // Don't allow long press on truncation message + if ([self _pendingTruncationTap]) { + return NO; + } + + // Ask our delegate if a long-press on an attribute is relevant + if ([self.delegate respondsToSelector:@selector(richTextNode:shouldLongPressLinkAttribute:value:)]) { + return [self.delegate richTextNode:self shouldLongPressLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue]; + } + + // Otherwise we are good to go. + return YES; + } + + if (([self _pendingLinkTap] || [self _pendingTruncationTap]) + && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] + && CGRectContainsPoint(self.view.bounds, [gestureRecognizer locationInView:self.view])) { + return NO; + } + + return [super gestureRecognizerShouldBegin:gestureRecognizer]; +} + +#pragma mark - Highlighting + +- (NSRange)highlightRange +{ + return _highlightRange; +} + +- (void)setHighlightRange:(NSRange)highlightRange +{ + [self setHighlightRange:highlightRange animated:NO]; +} + +- (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated +{ + [self _setHighlightRange:highlightRange forAttributeName:nil value:nil animated:animated]; +} + +- (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated +{ + ASDisplayNodeAssertMainThread(); + + _highlightedLinkAttributeName = highlightedAttributeName; + _highlightedLinkAttributeValue = highlightedAttributeValue; + + if (!NSEqualRanges(highlightRange, _highlightRange) && ((0 != highlightRange.length) || (0 != _highlightRange.length))) { + + _highlightRange = highlightRange; + + if (_activeHighlightLayer) { + if (animated) { + __unsafe_unretained CALayer *weakHighlightLayer = _activeHighlightLayer; + _activeHighlightLayer = nil; + + weakHighlightLayer.opacity = 0.0; + + CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeOut.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + fadeOut.fromValue = @(((CALayer *)weakHighlightLayer.presentationLayer).opacity); + fadeOut.toValue = @0.0; + fadeOut.fillMode = kCAFillModeBoth; + fadeOut.duration = ASTextNodeHighlightFadeOutDuration; + + dispatch_block_t prev = [CATransaction completionBlock]; + [CATransaction setCompletionBlock:^{ + [weakHighlightLayer removeFromSuperlayer]; + }]; + + [weakHighlightLayer addAnimation:fadeOut forKey:fadeOut.keyPath]; + + [CATransaction setCompletionBlock:prev]; + + } else { + [_activeHighlightLayer removeFromSuperlayer]; + _activeHighlightLayer = nil; + } + } + if (0 != highlightRange.length) { + // Find layer in hierarchy that allows us to draw highlighting on. + CALayer *highlightTargetLayer = self.layer; + while (highlightTargetLayer != nil) { + if (highlightTargetLayer.as_allowsHighlightDrawing) { + break; + } + highlightTargetLayer = highlightTargetLayer.superlayer; + } + + if (highlightTargetLayer != nil) { + NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextNodeRendererMeasureOptionBlock]; + NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; + for (NSValue *rectValue in highlightRects) { + CGRect rendererRect = [[self class] _adjustRendererRect:rectValue.CGRectValue forShadowPadding:_shadower.shadowPadding]; + CGRect highlightedRect = [self.layer convertRect:rendererRect toLayer:highlightTargetLayer]; + [converted addObject:[NSValue valueWithCGRect:highlightedRect]]; + } + + ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; + overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; + overlayLayer.frame = highlightTargetLayer.bounds; + overlayLayer.masksToBounds = NO; + overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; + [highlightTargetLayer addSublayer:overlayLayer]; + + if (animated) { + CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fadeIn.fromValue = @0.0; + fadeIn.toValue = @(overlayLayer.opacity); + fadeIn.duration = ASTextNodeHighlightFadeInDuration; + + [overlayLayer addAnimation:fadeIn forKey:fadeIn.keyPath]; + } + + [overlayLayer setNeedsDisplay]; + + _activeHighlightLayer = overlayLayer; + } + } + } +} + +- (void)_clearHighlightIfNecessary +{ + if ([self _pendingLinkTap] || [self _pendingTruncationTap]) { + [self setHighlightRange:NSMakeRange(0, 0) animated:YES]; + } +} + ++ (CGColorRef)_highlightColorForStyle:(ASTextNodeHighlightStyle)style +{ + return [UIColor colorWithWhite:(style == ASTextNodeHighlightStyleLight ? 0.0 : 1.0) alpha:1.0].CGColor; +} + ++ (CGFloat)_highlightOpacityForStyle:(ASTextNodeHighlightStyle)style +{ + return (style == ASTextNodeHighlightStyleLight) ? ASTextNodeHighlightLightOpacity : ASTextNodeHighlightDarkOpacity; +} + +#pragma mark - Text rects + ++ (CGRect)_adjustRendererRect:(CGRect)rendererRect forShadowPadding:(UIEdgeInsets)shadowPadding +{ + rendererRect.origin.x -= shadowPadding.left; + rendererRect.origin.y -= shadowPadding.top; + return rendererRect; +} + +- (NSArray *)_rectsForTextRange:(NSRange)textRange measureOption:(ASTextNodeRendererMeasureOption)measureOption +{ + NSArray *rects = [[self _renderer] rectsForTextRange:textRange measureOption:measureOption]; + NSMutableArray *adjustedRects = [NSMutableArray array]; + + for (NSValue *rectValue in rects) { + CGRect rect = [rectValue CGRectValue]; + rect = [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding]; + + NSValue *adjustedRectValue = [NSValue valueWithCGRect:rect]; + [adjustedRects addObject:adjustedRectValue]; + } + + return adjustedRects; +} + +- (NSArray *)rectsForTextRange:(NSRange)textRange +{ + return [self _rectsForTextRange:textRange measureOption:ASTextNodeRendererMeasureOptionCapHeight]; +} + +- (NSArray *)highlightRectsForTextRange:(NSRange)textRange +{ + return [self _rectsForTextRange:textRange measureOption:ASTextNodeRendererMeasureOptionBlock]; +} + +- (CGRect)trailingRect +{ + CGRect rect = [[self _renderer] trailingRect]; + return [self.class _adjustRendererRect:rect forShadowPadding:self.shadowPadding]; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + CGRect frame = [[self _renderer] frameForTextRange:textRange]; + return [self.class _adjustRendererRect:frame forShadowPadding:self.shadowPadding]; +} + +#pragma mark - Touch Handling + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + + ASDisplayNodeAssertMainThread(); + + UITouch *touch = [touches anyObject]; + + UIView *view = touch.view; + CGPoint point = [touch locationInView:view]; + point = [self.view convertPoint:point fromView:view]; + + NSRange range = NSMakeRange(0, 0); + NSString *linkAttributeName = nil; + BOOL inAdditionalTruncationMessage = NO; + + id linkAttributeValue = [self _linkAttributeValueAtPoint:point + attributeName:&linkAttributeName + range:&range + inAdditionalTruncationMessage:&inAdditionalTruncationMessage]; + + NSUInteger lastCharIndex = NSIntegerMax; + BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); + + if (inAdditionalTruncationMessage) { + NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:[[self _renderer] visibleRange]]; + [self _setHighlightRange:truncationMessageRange forAttributeName:ASTextNodeTruncationTokenAttributeName value:nil animated:YES]; + } else if (range.length && !linkCrossesVisibleRange && linkAttributeValue != nil && linkAttributeName != nil) { + [self _setHighlightRange:range forAttributeName:linkAttributeName value:linkAttributeValue animated:YES]; + } +} + + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; + + if ([self _pendingLinkTap] && [_delegate respondsToSelector:@selector(richTextNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint point = [[touches anyObject] locationInView:self.view]; + [_delegate richTextNode:self tappedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:point textRange:_highlightRange]; + } + + if ([self _pendingTruncationTap]) { + if ([_delegate respondsToSelector:@selector(richTextNodeTappedTruncationToken:)]) { + [_delegate richTextNodeTappedTruncationToken:self]; + } + } + + [self _clearHighlightIfNecessary]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; + + [self _clearHighlightIfNecessary]; +} + +- (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer +{ + // Respond to long-press when it begins, not when it ends. + if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { + if ([self.delegate respondsToSelector:@selector(richTextNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { + CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; + [self.delegate richTextNode:self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; + } + } +} + +- (BOOL)_pendingLinkTap +{ + return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && _delegate != nil; +} + +- (BOOL)_pendingTruncationTap +{ + return [_highlightedLinkAttributeName isEqualToString:ASTextNodeTruncationTokenAttributeName]; +} + +#pragma mark - Shadow Properties + +- (CGColorRef)shadowColor +{ + return _shadowColor; +} + +- (void)setShadowColor:(CGColorRef)shadowColor +{ + if (_shadowColor != shadowColor) { + if (shadowColor != NULL) { + CGColorRetain(shadowColor); + } + _shadowColor = shadowColor; + [self _invalidateShadower]; + [self setNeedsDisplay]; + } +} + +- (CGSize)shadowOffset +{ + return _shadowOffset; +} + +- (void)setShadowOffset:(CGSize)shadowOffset +{ + if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { + _shadowOffset = shadowOffset; + [self _invalidateShadower]; + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowOpacity +{ + return _shadowOpacity; +} + +- (void)setShadowOpacity:(CGFloat)shadowOpacity +{ + if (_shadowOpacity != shadowOpacity) { + _shadowOpacity = shadowOpacity; + [self _invalidateShadower]; + [self setNeedsDisplay]; + } +} + +- (CGFloat)shadowRadius +{ + return _shadowRadius; +} + +- (void)setShadowRadius:(CGFloat)shadowRadius +{ + if (_shadowRadius != shadowRadius) { + _shadowRadius = shadowRadius; + [self _invalidateShadower]; + [self setNeedsDisplay]; + } +} + +- (UIEdgeInsets)shadowPadding +{ + return [[self _shadower] shadowPadding]; +} + +#pragma mark - Truncation Message + +- (void)setTruncationAttributedString:(NSAttributedString *)truncationAttributedString +{ + // No-op if they're exactly equal (avoid redrawing) + if (_truncationAttributedString == truncationAttributedString) { + return; + } + + if (![_truncationAttributedString isEqual:truncationAttributedString]) { + _truncationAttributedString = [truncationAttributedString copy]; + [self _invalidateTruncationString]; + } +} + +- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage +{ + // Short circuit if we're setting to nil (prevent redrawing when we don't need to) + if (_additionalTruncationMessage == additionalTruncationMessage) { + return; + } + + if (![_additionalTruncationMessage isEqual:additionalTruncationMessage]) { + _additionalTruncationMessage = [additionalTruncationMessage copy]; + [self _invalidateTruncationString]; + } +} + +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + if (_truncationMode != truncationMode) { + _truncationMode = truncationMode; + [self _invalidateRenderer]; + [self setNeedsDisplay]; + } +} + +- (BOOL)isTruncated +{ + return [[self _renderer] truncationStringCharacterRange].location != NSNotFound; +} + +- (NSUInteger)lineCount +{ + return [[self _renderer] lineCount]; +} + +#pragma mark - Truncation Message + +- (void)_invalidateTruncationString +{ + _composedTruncationString = [self _prepareTruncationStringForDrawing:[self _composedTruncationString]]; + [self _invalidateRenderer]; + [self setNeedsDisplay]; +} + +/** + * @return the additional truncation message range within the as-rendered text. + * Must be called from main thread + */ +- (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange +{ + // Check if we even have an additional truncation message. + if (!_additionalTruncationMessage) { + return NSMakeRange(NSNotFound, 0); + } + + // Character location of the unicode ellipsis (the first index after the visible range) + NSInteger truncationTokenIndex = NSMaxRange(visibleRange); + + NSUInteger additionalTruncationMessageLength = _additionalTruncationMessage.length; + // We get the location of the truncation token, then add the length of the + // truncation attributed string +1 for the space between. + NSRange range = NSMakeRange(truncationTokenIndex + _truncationAttributedString.length + 1, additionalTruncationMessageLength); + return range; +} + +/** + * @return the truncation message for the string. If there are both an + * additional truncation message and a truncation attributed string, they will + * be properly composed. + */ +- (NSAttributedString *)_composedTruncationString +{ + // Short circuit if we only have one or the other. + if (!_additionalTruncationMessage) { + return _truncationAttributedString; + } + if (!_truncationAttributedString) { + return _additionalTruncationMessage; + } + + // If we've reached this point, both _additionalTruncationMessage and + // _truncationAttributedString are present. Compose them. + + NSMutableAttributedString *newComposedTruncationString = [[NSMutableAttributedString alloc] initWithAttributedString:_truncationAttributedString]; + [newComposedTruncationString replaceCharactersInRange:NSMakeRange(newComposedTruncationString.length, 0) withString:@" "]; + [newComposedTruncationString appendAttributedString:_additionalTruncationMessage]; + return newComposedTruncationString; +} + +/** + * - cleanses it of core text attributes so TextKit doesn't crash + * - Adds whole-string attributes so the truncation message matches the styling + * of the body text + */ +- (NSAttributedString *)_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString +{ + truncationString = ASCleanseAttributedStringOfCoreTextAttributes(truncationString); + NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; + // Grab the attributes from the full string + if (_attributedString.length > 0) { + NSAttributedString *originalString = _attributedString; + NSInteger originalStringLength = _attributedString.length; + // Add any of the original string's attributes to the truncation string, + // but don't overwrite any of the truncation string's attributes + NSDictionary *originalStringAttributes = [originalString attributesAtIndex:originalStringLength-1 effectiveRange:NULL]; + [truncationString enumerateAttributesInRange:NSMakeRange(0, truncationString.length) options:0 usingBlock: + ^(NSDictionary *attributes, NSRange range, BOOL *stop) { + NSMutableDictionary *futureTruncationAttributes = [NSMutableDictionary dictionaryWithDictionary:originalStringAttributes]; + [futureTruncationAttributes addEntriesFromDictionary:attributes]; + [truncationMutableString setAttributes:futureTruncationAttributes range:range]; + }]; + } + return truncationMutableString; +} + +@end diff --git a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch new file mode 100644 index 0000000000..625be4d28b --- /dev/null +++ b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch @@ -0,0 +1,9 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/AsyncDisplayKit/Details/ASHighlightOverlayLayer.h b/AsyncDisplayKit/Details/ASHighlightOverlayLayer.h new file mode 100644 index 0000000000..f57940afb3 --- /dev/null +++ b/AsyncDisplayKit/Details/ASHighlightOverlayLayer.h @@ -0,0 +1,43 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface ASHighlightOverlayLayer : CALayer + +/** + @summary Initializes with CGRects for the highlighting, in the targetLayer's coordinate space. + + @desc This is the designated initializer. + + @param rects Array containing CGRects wrapped in NSValue. + @param targetLayer The layer that the rects are relative to. The rects will be translated to the receiver's coordinate space when rendering. + */ +- (id)initWithRects:(NSArray *)rects targetLayer:(id)targetLayer; + +/** + @summary Initializes with CGRects for the highlighting, in the receiver's coordinate space. + + @param rects Array containing CGRects wrapped in NSValue. + */ +- (id)initWithRects:(NSArray *)rects; + +@property (atomic, strong) __attribute__((NSObject)) CGColorRef highlightColor; +@property (atomic, weak) CALayer *targetLayer; + +@end + +@interface CALayer (ASHighlightOverlayLayerSupport) + +/** + @summary Set to YES to indicate to a sublayer that this is where highlight overlay layers (for pressed states) should + be added so that the highlight won't be clipped by a neighboring layer. + */ +@property (nonatomic, assign, setter = as_setAllowsHighlightDrawing:) BOOL as_allowsHighlightDrawing; + +@end diff --git a/AsyncDisplayKit/Details/ASHighlightOverlayLayer.m b/AsyncDisplayKit/Details/ASHighlightOverlayLayer.m new file mode 100644 index 0000000000..68fc7383f0 --- /dev/null +++ b/AsyncDisplayKit/Details/ASHighlightOverlayLayer.m @@ -0,0 +1,130 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASHighlightOverlayLayer.h" + +#import + +static const CGFloat kCornerRadius = 2.5; +static const UIEdgeInsets padding = {2, 4, 1.5, 4}; + +@implementation ASHighlightOverlayLayer +{ + NSArray *_rects; +} + ++ (id)defaultValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"contentsScale"]) { + return [NSNumber numberWithFloat:[[UIScreen mainScreen] scale]]; + } else if ([key isEqualToString:@"highlightColor"]) { + CGFloat components[] = {0, 0, 0, 0.25}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorRef color = CGColorCreate(colorSpace, components); + CGColorSpaceRelease(colorSpace); + return CFBridgingRelease(color); + } else { + return [super defaultValueForKey:key]; + } +} + ++ (BOOL)needsDisplayForKey:(NSString *)key +{ + if ([key isEqualToString:@"bounds"]) { + return YES; + } else { + return [super needsDisplayForKey:key]; + } +} + ++ (id)defaultActionForKey:(NSString *)event +{ + return (id)[NSNull null]; +} + +- (id)initWithRects:(NSArray *)rects +{ + return [self initWithRects:rects targetLayer:nil]; +} + +- (id)initWithRects:(NSArray *)rects targetLayer:(id)targetLayer +{ + if (self = [super init]) { + _rects = [rects copy]; + _targetLayer = targetLayer; + } + return self; +} + +@dynamic highlightColor; + +- (void)drawInContext:(CGContextRef)ctx +{ + [super drawInContext:ctx]; + + CGAffineTransform affine = CGAffineTransformIdentity; + CGMutablePathRef highlightPath = CGPathCreateMutable(); + CALayer *targetLayer = self.targetLayer; + + for (NSValue *value in _rects) { + CGRect rect = [value CGRectValue]; + + // Don't highlight empty rects. + if (CGRectIsEmpty(rect)) { + continue; + } + + if (targetLayer != nil) { + rect = [self convertRect:rect fromLayer:targetLayer]; + } + rect = CGRectMake(roundf(rect.origin.x), roundf(rect.origin.y), roundf(rect.size.width), roundf(rect.size.height)); + + CGFloat minX = rect.origin.x - padding.left; + CGFloat maxX = CGRectGetMaxX(rect) + padding.right; + CGFloat midX = (maxX - minX) / 2 + minX; + CGFloat minY = rect.origin.y - padding.top; + CGFloat maxY = CGRectGetMaxY(rect) + padding.bottom; + CGFloat midY = (maxY - minY) / 2 + minY; + + CGPathMoveToPoint(highlightPath, &affine, minX, midY); + CGPathAddArcToPoint(highlightPath, &affine, minX, maxY, midX, maxY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, maxX, maxY, maxX, midY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, maxX, minY, midX, minY, kCornerRadius); + CGPathAddArcToPoint(highlightPath, &affine, minX, minY, minX, midY, kCornerRadius); + CGPathCloseSubpath(highlightPath); + } + + CGContextAddPath(ctx, highlightPath); + CGContextSetFillColorWithColor(ctx, self.highlightColor); + CGContextDrawPath(ctx, kCGPathFill); + CGPathRelease(highlightPath); +} + +- (CALayer *)hitTest:(CGPoint)p +{ + // Don't handle taps + return nil; +} + +@end + +@implementation CALayer (ASHighlightOverlayLayerSupport) + +static NSString *kAllowsHighlightDrawingKey = @"allows_highlight_drawing"; + +- (BOOL)as_allowsHighlightDrawing +{ + return [[self valueForKey:kAllowsHighlightDrawingKey] boolValue]; +} + +- (void)as_setAllowsHighlightDrawing:(BOOL)allowsHighlightDrawing +{ + [self setValue:@(allowsHighlightDrawing) forKey:kAllowsHighlightDrawingKey]; +} + +@end diff --git a/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.h b/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.h new file mode 100644 index 0000000000..f801e28961 --- /dev/null +++ b/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.h @@ -0,0 +1,58 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/* + * Use this class to compose new attributed strings. You may use the normal + * attributed string calls on this the same way you would on a normal mutable + * attributed string, but it coalesces your changes into transactions on the + * actual string allowing improvements in performance. + * + * @discussion This is a use-once and throw away class for each string you make. + * Since this class is designed for increasing performance, we actually hand + * back the internally managed mutable attributed string in the + * `composedAttributedString` call. So once you make that call, any more + * changes will actually modify the string that was handed back to you in that + * method. + * + * Combination of multiple calls into single attribution is managed through + * merging of attribute dictionaries over ranges. For best performance, call + * collections of attributions over a single range together. So for instance, + * don't call addAttributes for range1, then range2, then range1 again. Group + * them together so you call addAttributes for both range1 together, and then + * range2. + * + * Also please note that switching between addAttribute and setAttributes in the + * middle of composition is a bad idea for performance because they have + * semantically different meanings, and trigger a commit of the pending + * attributes. + * + * Please note that ALL of the standard NSString methods are left unimplemented. + */ +@interface ASMutableAttributedStringBuilder : NSMutableAttributedString + +- (instancetype)initWithString:(NSString *)str attributes:(NSDictionary *)attrs; +- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr; + +- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str; +- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range; + +- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range; +- (void)addAttributes:(NSDictionary *)attrs range:(NSRange)range; +- (void)removeAttribute:(NSString *)name range:(NSRange)range; + +- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString; +- (void)insertAttributedString:(NSAttributedString *)attrString atIndex:(NSUInteger)loc; +- (void)appendAttributedString:(NSAttributedString *)attrString; +- (void)deleteCharactersInRange:(NSRange)range; +- (void)setAttributedString:(NSAttributedString *)attrString; + +- (NSMutableAttributedString *)composedAttributedString; + +@end diff --git a/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.m b/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.m new file mode 100644 index 0000000000..d8fbd2fb42 --- /dev/null +++ b/AsyncDisplayKit/Details/ASMutableAttributedStringBuilder.m @@ -0,0 +1,253 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASMutableAttributedStringBuilder.h" + +@implementation ASMutableAttributedStringBuilder { + // Flag for the type of the current transaction (set or add) + BOOL _setRange; + // The range over which the currently pending transaction will occur + NSRange _pendingRange; + // The actual attribute dictionary that is being composed + NSMutableDictionary *_pendingRangeAttributes; + NSMutableAttributedString *_attrStr; + + // We delay initialization of the _attrStr until we need to + NSString *_initString; +} + +- (instancetype)init +{ + if (self = [super init]) { + _attrStr = [[NSMutableAttributedString alloc] init]; + _pendingRange.location = NSNotFound; + } + return self; +} + +- (instancetype)initWithString:(NSString *)str +{ + return [self initWithString:str attributes:@{}]; +} + +- (instancetype)initWithString:(NSString *)str attributes:(NSDictionary *)attrs +{ + if (self = [super init]) { + // We cache this in an ivar that we can lazily construct the attributed + // string with when we get to a forced commit point. + _initString = str; + // Triggers a creation of the _pendingRangeAttributes dictionary which then + // is filled with entries from the given attrs dict. + [[self _pendingRangeAttributes] addEntriesFromDictionary:attrs]; + _setRange = NO; + _pendingRange = NSMakeRange(0, _initString.length); + } + return self; +} + +- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr +{ + if (self = [super init]) { + _attrStr = [[NSMutableAttributedString alloc] initWithAttributedString:attrStr]; + _pendingRange.location = NSNotFound; + } + return self; +} + +- (NSMutableAttributedString *)_attributedString +{ + if (_attrStr == nil && _initString != nil) { + // We can lazily construct the attributed string if it hasn't already been + // created with the existing pending attributes. This is significantly + // faster if more attributes are added after initializing this instance + // and the new attributions are for the entire string anyway. + _attrStr = [[NSMutableAttributedString alloc] initWithString:_initString attributes:_pendingRangeAttributes]; + _pendingRangeAttributes = nil; + _pendingRange.location = NSNotFound; + _initString = nil; + } + + return _attrStr; +} + +#pragma mark - Pending attribution + +- (NSMutableDictionary *)_pendingRangeAttributes +{ + // Lazy dictionary creation. Call this if you want to force initialization, + // otherwise just use the ivar. + if (_pendingRangeAttributes == nil) { + _pendingRangeAttributes = [[NSMutableDictionary alloc] init]; + } + return _pendingRangeAttributes; +} + +- (void)_applyPendingRangeAttributions +{ + if (_attrStr == nil) { + // Trigger its creation if it doesn't exist. + [self _attributedString]; + } + + if (_pendingRangeAttributes.count == 0) { + return; + } + + if (_pendingRange.location == NSNotFound) { + return; + } + + if (_setRange) { + [[self _attributedString] setAttributes:_pendingRangeAttributes range:_pendingRange]; + } else { + [[self _attributedString] addAttributes:_pendingRangeAttributes range:_pendingRange]; + } + _pendingRangeAttributes = nil; + _pendingRange.location = NSNotFound; +} + +#pragma mark - Editing + +- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] replaceCharactersInRange:range withString:str]; +} + +- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] replaceCharactersInRange:range withAttributedString:attrString]; +} + +- (void)addAttribute:(NSString *)name value:(id)value range:(NSRange)range +{ + if (_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = NO; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + pendingAttributes[name] = value; +} + +- (void)addAttributes:(NSDictionary *)attrs range:(NSRange)range +{ + if (_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = NO; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + [pendingAttributes addEntriesFromDictionary:attrs]; +} + +- (void)insertAttributedString:(NSAttributedString *)attrString atIndex:(NSUInteger)loc +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] insertAttributedString:attrString atIndex:loc]; +} + +- (void)appendAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] appendAttributedString:attrString]; +} + +- (void)deleteCharactersInRange:(NSRange)range +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] deleteCharactersInRange:range]; +} + +- (void)setAttributedString:(NSAttributedString *)attrString +{ + [self _applyPendingRangeAttributions]; + [[self _attributedString] setAttributedString:attrString]; +} + +- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range +{ + if (!_setRange) { + [self _applyPendingRangeAttributions]; + _setRange = YES; + } + + if (!NSEqualRanges(_pendingRange, range)) { + [self _applyPendingRangeAttributions]; + _pendingRange = range; + } + + NSMutableDictionary *pendingAttributes = [self _pendingRangeAttributes]; + [pendingAttributes addEntriesFromDictionary:attrs]; +} + +- (void)removeAttribute:(NSString *)name range:(NSRange)range +{ + // This call looks like the other set/add functions, but in order for this + // function to perform as advertised we MUST first add the attributes we + // currently have pending. + [self _applyPendingRangeAttributions]; + + [[self _attributedString] removeAttribute:name range:range]; +} + +#pragma mark - Output + +- (NSMutableAttributedString *)composedAttributedString +{ + if (_pendingRangeAttributes.count > 0) { + [self _applyPendingRangeAttributions]; + } + return [self _attributedString]; +} + +#pragma mark - Forwarding + +- (NSUInteger)length +{ + // If we just want a length call, no need to lazily construct the attributed string + return _attrStr ? _attrStr.length : _initString.length; +} + +- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range +{ + return [[self _attributedString] attributesAtIndex:location effectiveRange:range]; +} + +- (NSString *)string +{ + return _attrStr ? _attrStr.string : _initString; +} + +- (NSMutableString *)mutableString +{ + return [[self _attributedString] mutableString]; +} + +- (void)beginEditing +{ + [[self _attributedString] beginEditing]; +} + +- (void)endEditing +{ + [[self _attributedString] endEditing]; +} + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.h b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.h new file mode 100644 index 0000000000..5e9afde6b4 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.h @@ -0,0 +1,82 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import + +ASDISPLAYNODE_EXTERN_C_BEGIN +/** + @abstract Returns whether a given attribute is an unsupported Core Text attribute. + @param attributeName The name of the attribute + @discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion: + - kCTForegroundColorFromContextAttributeName + - kCTSuperscriptAttributeName + - kCTGlyphInfoAttributeName + - kCTCharacterShapeAttributeName + - kCTLanguageAttributeName + - kCTRunDelegateAttributeName + - kCTBaselineClassAttributeName + - kCTBaselineInfoAttributeName + - kCTBaselineReferenceInfoAttributeName + - kCTWritingDirectionAttributeName + - kCTUnderlineColorAttributeName + @result Whether attributeName is an unsupported Core Text attribute. + */ +BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName); + + +/** + @abstract Returns an attributes dictionary for use by NSAttributedString, given a dictionary of Core Text attributes. + @param coreTextAttributes An NSDictionary whose keys are CFAttributedStringRef attributes. + @discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion: + - kCTForegroundColorFromContextAttributeName + - kCTSuperscriptAttributeName + - kCTGlyphInfoAttributeName + - kCTCharacterShapeAttributeName + - kCTLanguageAttributeName + - kCTRunDelegateAttributeName + - kCTBaselineClassAttributeName + - kCTBaselineInfoAttributeName + - kCTBaselineReferenceInfoAttributeName + - kCTWritingDirectionAttributeName + - kCTUnderlineColorAttributeName + @result An NSDictionary of attributes for use by NSAttributedString. + */ +extern NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes); + +/** + @abstract Returns an NSAttributedString whose Core Text attributes have been converted, where possible, to NSAttributedString attributes. + @param dirtyAttributedString An NSAttributedString that may contain Core Text attributes. + @result An NSAttributedString that's preserved as many CFAttributedString attributes as possible. + */ +extern NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString); + +ASDISPLAYNODE_EXTERN_C_END + +#pragma mark - +#pragma mark - +@interface NSParagraphStyle (ASTextNodeCoreTextAdditions) + +/** + @abstract Returns an NSParagraphStyle initialized with the paragraph specifiers from the given CTParagraphStyleRef. + @param coreTextParagraphStyle A Core Text paragraph style. + @discussion It is important to note that not all CTParagraphStyle specifiers are supported by NSParagraphStyle, and consequently, this is a lossy conversion. Notably, the following specifiers will not preserved: + - kCTParagraphStyleSpecifierTabStops + - kCTParagraphStyleSpecifierDefaultTabInterval + - kCTParagraphStyleSpecifierMaximumLineSpacing + - kCTParagraphStyleSpecifierMinimumLineSpacing + - kCTParagraphStyleSpecifierLineSpacingAdjustment + - kCTParagraphStyleSpecifierLineBoundsOptions + @result An NSParagraphStyle initializd with as many of the paragraph specifiers from `coreTextParagraphStyle` as possible. + + */ ++ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle; + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m new file mode 100644 index 0000000000..3ea9d596f2 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m @@ -0,0 +1,239 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNodeCoreTextAdditions.h" + +#import +#import + +#pragma mark - Public +BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) +{ + static NSSet *coreTextAttributes; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + coreTextAttributes = [NSSet setWithObjects:(__bridge id)kCTForegroundColorAttributeName, + kCTForegroundColorFromContextAttributeName, + kCTForegroundColorAttributeName, + kCTStrokeColorAttributeName, + kCTUnderlineStyleAttributeName, + kCTVerticalFormsAttributeName, + kCTRunDelegateAttributeName, + kCTBaselineClassAttributeName, + kCTBaselineInfoAttributeName, + kCTBaselineReferenceInfoAttributeName, + kCTUnderlineColorAttributeName, + nil]; + }); + return [coreTextAttributes containsObject:attributeName]; +} + +NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes) +{ + NSMutableDictionary *cleanAttributes = [[NSMutableDictionary alloc] initWithCapacity:coreTextAttributes.count]; + + [coreTextAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *stop) { + // The following attributes are not supported on NSAttributedString. Should they become available, we should add them. + /* + kCTForegroundColorFromContextAttributeName + kCTSuperscriptAttributeName + kCTGlyphInfoAttributeName + kCTCharacterShapeAttributeName + kCTLanguageAttributeName + kCTRunDelegateAttributeName + kCTBaselineClassAttributeName + kCTBaselineInfoAttributeName + kCTBaselineReferenceInfoAttributeName + kCTWritingDirectionAttributeName + kCTUnderlineColorAttributeName + */ + + // Conversely, the following attributes are not supported on CFAttributedString. Should they become available, we should add them. + /* + NSStrikethroughStyleAttributeName + NSShadowAttributeName + NSBackgroundColorAttributeName + */ + + // kCTFontAttributeName -> NSFontAttributeName + if ([coreTextKey isEqualToString:(NSString *)kCTFontAttributeName]) { + CTFontRef coreTextFont = (__bridge CTFontRef)coreTextValue; + NSString *fontName = (__bridge_transfer NSString *)CTFontCopyPostScriptName(coreTextFont); + CGFloat fontSize = CTFontGetSize(coreTextFont); + + cleanAttributes[NSFontAttributeName] = [UIFont fontWithName:fontName size:fontSize]; + } + // kCTKernAttributeName -> NSKernAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTKernAttributeName]) { + cleanAttributes[NSKernAttributeName] = (NSNumber *)coreTextValue; + } + // kCTLigatureAttributeName -> NSLigatureAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTLigatureAttributeName]) { + cleanAttributes[NSLigatureAttributeName] = (NSNumber *)coreTextValue; + } + // kCTForegroundColorAttributeName -> NSForegroundColorAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTForegroundColorAttributeName]) { + cleanAttributes[NSForegroundColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue]; + } + // kCTParagraphStyleAttributeName -> NSParagraphStyleAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTParagraphStyleAttributeName]) { + cleanAttributes[NSParagraphStyleAttributeName] = [NSParagraphStyle paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextValue]; + } + // kCTStrokeWidthAttributeName -> NSStrokeWidthAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeWidthAttributeName]) { + cleanAttributes[NSStrokeWidthAttributeName] = (NSNumber *)coreTextValue; + } + // kCTStrokeColorAttributeName -> NSStrokeColorAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeColorAttributeName]) { + cleanAttributes[NSStrokeColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue]; + } + // kCTUnderlineStyleAttributeName -> NSUnderlineStyleAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTUnderlineStyleAttributeName]) { + cleanAttributes[NSUnderlineStyleAttributeName] = (NSNumber *)coreTextValue; + } + // kCTVerticalFormsAttributeName -> NSVerticalGlyphFormAttributeName + else if ([coreTextKey isEqualToString:(NSString *)kCTVerticalFormsAttributeName]) { + BOOL flag = (BOOL)CFBooleanGetValue((CFBooleanRef)coreTextValue); + cleanAttributes[NSVerticalGlyphFormAttributeName] = @((int)flag); // NSVerticalGlyphFormAttributeName is documented to be an NSNumber with an integer that's either 0 or 1. + } + // Don't filter out any internal text attributes + else if (!ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)){ + cleanAttributes[coreTextKey] = coreTextValue; + } + }]; + + return cleanAttributes; +} + +NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString) +{ + if (!dirtyAttributedString) + return nil; + + // First see if there are any core text attributes on the string + __block BOOL containsCoreTextAttributes = NO; + [dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, dirtyAttributedString.length) + options:0 + usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) { + [dirtyAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *innerStop) { + if (ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)) { + containsCoreTextAttributes = YES; + *innerStop = YES; + } + }]; + *stop = containsCoreTextAttributes; + }]; + if (containsCoreTextAttributes) { + + NSString *plainString = dirtyAttributedString.string; + NSMutableAttributedString *cleanAttributedString = [[NSMutableAttributedString alloc] initWithString:plainString]; + + // Iterate over all of the attributes, cleaning them as appropriate and applying them as we go. + [dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, plainString.length) + options:0 + usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) { + [cleanAttributedString addAttributes:NSAttributedStringAttributesForCoreTextAttributes(dirtyAttributes) range:range]; + }]; + + return cleanAttributedString; + } else { + return dirtyAttributedString; + } +} + +#pragma mark - +#pragma mark - +@implementation NSParagraphStyle (ASTextNodeCoreTextAdditions) + ++ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle; +{ + NSMutableParagraphStyle *newParagraphStyle = [[NSMutableParagraphStyle alloc] init]; + + if (!coreTextParagraphStyle) + return newParagraphStyle; + + // The following paragraph style specifiers are not supported on NSParagraphStyle. Should they become available, we should add them. + /* + kCTParagraphStyleSpecifierTabStops + kCTParagraphStyleSpecifierDefaultTabInterval + kCTParagraphStyleSpecifierMaximumLineSpacing + kCTParagraphStyleSpecifierMinimumLineSpacing + kCTParagraphStyleSpecifierLineSpacingAdjustment + kCTParagraphStyleSpecifierLineBoundsOptions + */ + + // Conversely, the following paragraph styles are not supported on CTParagraphStyle. Should they become available, we should add them. + /* + hyphenationFactor + */ + + // kCTParagraphStyleSpecifierAlignment -> alignment + CTTextAlignment coreTextAlignment; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierAlignment, sizeof(coreTextAlignment), &coreTextAlignment)) + newParagraphStyle.alignment = NSTextAlignmentFromCTTextAlignment(coreTextAlignment); + + // kCTParagraphStyleSpecifierFirstLineHeadIndent -> firstLineHeadIndent + CGFloat firstLineHeadIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(firstLineHeadIndent), &firstLineHeadIndent)) + newParagraphStyle.firstLineHeadIndent = firstLineHeadIndent; + + // kCTParagraphStyleSpecifierHeadIndent -> headIndent + CGFloat headIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(headIndent), &headIndent)) + newParagraphStyle.headIndent = headIndent; + + // kCTParagraphStyleSpecifierTailIndent -> tailIndent + CGFloat tailIndent; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(tailIndent), &tailIndent)) + newParagraphStyle.tailIndent = tailIndent; + + // kCTParagraphStyleSpecifierLineBreakMode -> lineBreakMode + CTLineBreakMode coreTextLineBreakMode; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(coreTextLineBreakMode), &coreTextLineBreakMode)) + newParagraphStyle.lineBreakMode = (NSLineBreakMode)coreTextLineBreakMode; // They're the same enum. + + // kCTParagraphStyleSpecifierLineHeightMultiple -> lineHeightMultiple + CGFloat lineHeightMultiple; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(lineHeightMultiple), &lineHeightMultiple)) + newParagraphStyle.lineHeightMultiple = lineHeightMultiple; + + // kCTParagraphStyleSpecifierMaximumLineHeight -> maximumLineHeight + CGFloat maximumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(maximumLineHeight), &maximumLineHeight)) + newParagraphStyle.maximumLineHeight = maximumLineHeight; + + // kCTParagraphStyleSpecifierMinimumLineHeight -> minimumLineHeight + CGFloat minimumLineHeight; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(minimumLineHeight), &minimumLineHeight)) + newParagraphStyle.minimumLineHeight = minimumLineHeight; + + // kCTParagraphStyleSpecifierLineSpacing -> lineSpacing + // Note that kCTParagraphStyleSpecifierLineSpacing is deprecated and will die soon. We should not be using it. + CGFloat lineSpacing; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(lineSpacing), &lineSpacing)) + newParagraphStyle.lineSpacing = lineSpacing; + + // kCTParagraphStyleSpecifierParagraphSpacing -> paragraphSpacing + CGFloat paragraphSpacing; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(paragraphSpacing), ¶graphSpacing)) + newParagraphStyle.paragraphSpacing = paragraphSpacing; + + // kCTParagraphStyleSpecifierParagraphSpacingBefore -> paragraphSpacingBefore + CGFloat paragraphSpacingBefore; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphSpacingBefore), ¶graphSpacingBefore)) + newParagraphStyle.paragraphSpacingBefore = paragraphSpacingBefore; + + // kCTParagraphStyleSpecifierBaseWritingDirection -> baseWritingDirection + CTWritingDirection coreTextBaseWritingDirection; + if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(coreTextBaseWritingDirection), &coreTextBaseWritingDirection)) + newParagraphStyle.baseWritingDirection = (NSWritingDirection)coreTextBaseWritingDirection; // They're the same enum. + + return newParagraphStyle; +} + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeRenderer.h b/AsyncDisplayKit/Details/ASTextNodeRenderer.h new file mode 100644 index 0000000000..2ed4a8aeaf --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeRenderer.h @@ -0,0 +1,181 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef void (^as_renderer_index_block_t)(NSUInteger characterIndex, + CGRect glyphBoundingRect, + BOOL *stop); + +/* + * Measure options are used to specify which type of line height measurement to + * use. + * + * ASTextNodeRendererMeasureOptionLineHeight is faster and will give the + * height from the baseline to the next line. + * + * ASTextNodeRendererMeasureOptionCapHeight is a more nuanced measure of the + * glyphs in the given range that attempts to produce a visually balanced + * rectangle above and below the glyphs to produce nice looking text highlights. + * + * ASTextNodeRendererMeasureOptionBlock uses the cap height option to + * generate each glyph index, but combines all but the first and last line rect + * into a single block. Looks nice for multiline selection. + * + */ +typedef NS_ENUM(NSUInteger, ASTextNodeRendererMeasureOption) { + ASTextNodeRendererMeasureOptionLineHeight, + ASTextNodeRendererMeasureOptionCapHeight, + ASTextNodeRendererMeasureOptionBlock +}; + +/* + * This is an immutable textkit renderer that is responsible for sizing and + * rendering text. + * + * @discussion This class implements internal locking to allow it to be used + * safely from background threads. It is recommended that you create and cache a + * renderer for each combination of parameters. + */ +@interface ASTextNodeRenderer : NSObject + +/* + * Designated Initializer + * + * @discussion No sizing occurs as a result of initializing a renderer. + * Instead, sizing and truncation operations occur lazily as they are needed, + * so feel free + */ +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + truncationString:(NSAttributedString *)truncationString + truncationMode:(NSLineBreakMode)truncationMode + constrainedSize:(CGSize)constrainedSize; +#pragma mark - Drawing +/* + * Draw the renderer's text content into the bounds provided. + * + * @param bounds The rect in which to draw the contents of the renderer. + * @param isRasterizing If YES, the renderer will not draw its background color + * within the bounds. + * + * @discussion Note that if a shadow is to be drawn, then the text will actually + * draw inside a region that is inset from the bounds provided. Use + * shadowPadding to properly transform the bounds such that this is correct for + * your use-case. See shadowPadding docs for more. + * + * Initializes the textkit components lazily if they have not yet been created. + * You may want to consider triggering this cost before hitting the draw method + * if you are sensitive to this cost in drawInRect... + */ +- (void)drawInRect:(CGRect)bounds isRasterizing:(BOOL)isRasterizing; + +#pragma mark - Layout + +/* + * Returns the computed size of the renderer given the constrained size and + * other parameters in the initializer. + * + * @discussion No actual computation is done in this method. It simply returns + * the cached calculated size from initialization so this is very cheap to call. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (CGSize)size; + +/* + * Returns the trailing rect unused by the renderer in the last rendered line. + * + * @discussion In the coordinate space of the renderer. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (CGRect)trailingRect; + +/* + * Returns the bounding rect for the given character range. + * + * @param textRange The character range for which the bounding rect will be + * computed. Should be within the range of the attributedString of this + * renderer. + * + * @discussion In the coordinate space of the renderer. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (CGRect)frameForTextRange:(NSRange)textRange; + +/* + * Returns an array of rects representing the lines in the given character range + * + * @param textRange The character range for which the rects will be computed. + * should be within the range of the attributedString of this renderer. + * @param measureOption The measure option to use for construction of the rects. + * see ASTextNodeRendererMeasureOption docs for usage. + * + * @discussion This method is useful for providing highlighting text. Returned + * rects are in the coordinate space of the renderer. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange + measureOption:(ASTextNodeRendererMeasureOption)measureOption; + +/* + * Enumerate the text character indexes at a position within the coordinate + * space of the renderer. + * + * @param position The point inside the coordinate space of the renderer at + * which text indexes will be enumerated. + * @param block The block that will be executed for each index identified that + * may correspond to the given position. The block is given the character index + * that corresponds to the glyph at each index in question, as well as the + * bounding rect for that glyph. + * + * @discussion Glyph location based on a touch point is not an exact science + * because user touches are not well-represented by a simple point, especially + * in the context of link-heavy text. So we have this method to make it a bit + * easier. This method checks a grid of candidate positions around the touch + * point you give it, and computes the bounding rect of the glyph corresponding + * to the character index given. + * + * The bounding rect of the glyph can be used to identify the best glyph index + * that corresponds to your touch. For instance, comparing centroidal distance + * from the glyph bounding rect to the touch center is useful for identifying + * which link a user actually intended to select. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (void)enumerateTextIndexesAtPosition:(CGPoint)position + usingBlock:(as_renderer_index_block_t)block; + +#pragma mark - Text Ranges + +/* + * The character range that represents the truncationString provided in the + * initializer. location will be NSNotFound if no truncation occurred. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSRange)truncationStringCharacterRange; + +/* + * The character range from the original attributedString that is displayed by + * the renderer given the parameters in the initializer. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSRange)visibleRange; + +/* + * The number of lines shown in the string. + * + * Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSUInteger)lineCount; + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeRenderer.mm b/AsyncDisplayKit/Details/ASTextNodeRenderer.mm new file mode 100644 index 0000000000..6ade3a0cfc --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeRenderer.mm @@ -0,0 +1,621 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNodeRenderer.h" + +#import + +#import "ASAssert.h" +#import "ASTextNodeTextKitHelpers.h" +#import "ASTextNodeWordKerner.h" +#import "ASThread.h" + +static const CGFloat ASTextNodeRendererGlyphTouchHitSlop = 5.0; +static const CGFloat ASTextNodeRendererTextCapHeightPadding = 1.3; + +@interface ASTextNodeRenderer () + +@end + +@implementation ASTextNodeRenderer { + CGSize _constrainedSize; + CGSize _calculatedSize; + + NSAttributedString *_attributedString; + NSAttributedString *_truncationString; + NSLineBreakMode _truncationMode; + NSRange _truncationCharacterRange; + NSRange _visibleRange; + + ASTextNodeWordKerner *_wordKerner; + + ASDN::RecursiveMutex _textKitLock; + NSLayoutManager *_layoutManager; + NSTextStorage *_textStorage; + NSTextContainer *_textContainer; +} + +#pragma mark - Initialization + +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + truncationString:(NSAttributedString *)truncationString + truncationMode:(NSLineBreakMode)truncationMode + constrainedSize:(CGSize)constrainedSize +{ + if (self = [super init]) { + _attributedString = attributedString; + _truncationString = truncationString; + _truncationMode = truncationMode; + _truncationCharacterRange = NSMakeRange(NSNotFound, truncationString.length); + + _constrainedSize = constrainedSize; + } + return self; +} + +/* + * Use this method to lazily construct the TextKit components. + */ +- (void)_initializeTextKitComponentsIfNeeded +{ + ASDN::MutexLocker l(_textKitLock); + + if (_layoutManager == nil) { + [self _initializeTextKitComponentsWithAttributedString:_attributedString]; + } +} + +- (void)_initializeTextKitComponentsWithAttributedString:(NSAttributedString *)attributedString +{ + ASDN::MutexLocker l(_textKitLock); + + // Create the TextKit component stack with our default configuration. + _textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]); + _layoutManager = [[NSLayoutManager alloc] init]; + _layoutManager.usesFontLeading = NO; + _wordKerner = [[ASTextNodeWordKerner alloc] init]; + _layoutManager.delegate = _wordKerner; + [_textStorage addLayoutManager:_layoutManager]; + _textContainer = [[NSTextContainer alloc] initWithSize:_constrainedSize]; + // We want the text laid out up to the very edges of the container. + _textContainer.lineFragmentPadding = 0; + // Translate our truncation mode into a line break mode on the container + _textContainer.lineBreakMode = _truncationMode; + + [_layoutManager addTextContainer:_textContainer]; + + [self _invalidateLayout]; +} + +#pragma mark - Layout Initialization + +- (void)_invalidateLayout +{ + ASDN::MutexLocker l(_textKitLock); + + // Force a layout, which means we have to recompute our truncation parameters + NSInteger originalStringLength = _textStorage.string.length; + + [self _calculateSize]; + + NSRange visibleGlyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; + _visibleRange = [_layoutManager characterRangeForGlyphRange:visibleGlyphRange actualGlyphRange:NULL]; + + // Check if text is truncated, and if so apply our truncation string + if (_visibleRange.length < originalStringLength && _truncationString.length > 0) { + NSInteger firstCharacterIndexToReplace = [self _calculateCharacterIndexBeforeTruncationMessage]; + if (firstCharacterIndexToReplace == 0 || firstCharacterIndexToReplace == NSNotFound) { + // Something went horribly wrong, short-circuit + [self _calculateSize]; + return; + } + + // Update/truncate the visible range of text + _visibleRange = NSMakeRange(0, firstCharacterIndexToReplace); + NSRange truncationReplacementRange = NSMakeRange(firstCharacterIndexToReplace, _textStorage.length - firstCharacterIndexToReplace); + // Replace the end of the visible message with the truncation string + [_textStorage replaceCharactersInRange:truncationReplacementRange + withAttributedString:_truncationString]; + + _truncationCharacterRange = NSMakeRange(firstCharacterIndexToReplace, _truncationString.length); + + // We must recompute the calculated size because we may have changed it in + // changing the string + [self _calculateSize]; + } +} + +#pragma mark - Sizing + +/* + * Calculates the size of the text in the renderer based on the parameters + * stored in the ivars of this class. + * + * This method can be expensive, so it is important that it not be called + * frequently. It not only sizes the text, but it also configures the TextKit + * components for drawing, and responding to all other queries made to this + * class. + */ +- (void)_calculateSize +{ + ASDN::MutexLocker l(_textKitLock); + + [self _initializeTextKitComponentsIfNeeded]; + + + // Force glyph generation and layout, which may not have happened yet (and + // isn't triggered by -usedRectForTextContainer:). + [_layoutManager ensureLayoutForTextContainer:_textContainer]; + + CGRect constrainedRect = CGRect{CGPointZero, _constrainedSize}; + CGRect boundingRect = [_layoutManager usedRectForTextContainer:_textContainer]; + + // TextKit often returns incorrect glyph bounding rects in the horizontal + // direction, so we clip to our bounding rect to make sure our width + // calculations aren't being offset by glyphs going beyond the constrained + // rect. + boundingRect = CGRectIntersection(boundingRect, (CGRect){.size = constrainedRect.size}); + + _calculatedSize = boundingRect.size; +} + +- (CGSize)size +{ + [self _initializeTextKitComponentsIfNeeded]; + + return _calculatedSize; +} + +#pragma mark - Layout + +- (CGRect)trailingRect +{ + ASDN::MutexLocker l(_textKitLock); + + [self _initializeTextKitComponentsIfNeeded]; + + // If have an empty string, then our whole bounds constitute trailing space. + if ([_textStorage length] == 0) { + return CGRectMake(0, 0, _calculatedSize.width, _calculatedSize.height); + } + + // Take everything after our final character as trailing space. + NSArray *finalRects = [self rectsForTextRange:NSMakeRange([_textStorage length] - 1, 1) measureOption:ASTextNodeRendererMeasureOptionLineHeight]; + CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue]; + CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect)); + CGSize size = CGSizeMake(_calculatedSize.width - origin.x, _calculatedSize.height - origin.y); + return (CGRect){origin, size}; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + ASDN::MutexLocker l(_textKitLock); + + [self _initializeTextKitComponentsIfNeeded]; + + // Bail on invalid range. + if (NSMaxRange(textRange) > [_textStorage length]) { + ASDisplayNodeAssertNotNil(nil, @"Invalid range"); + return CGRectZero; + } + + // Force glyph generation and layout. + [_layoutManager ensureLayoutForTextContainer:_textContainer]; + + NSRange glyphRange = [_layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + CGRect textRect = [_layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textContainer]; + return textRect; +} + +- (NSArray *)rectsForTextRange:(NSRange)textRange + measureOption:(ASTextNodeRendererMeasureOption)measureOption +{ + ASDN::MutexLocker l(_textKitLock); + + [self _initializeTextKitComponentsIfNeeded]; + + BOOL textRangeIsValid = (NSMaxRange(textRange) <= [_textStorage length]); + ASDisplayNodeAssertTrue(textRangeIsValid); + if (!textRangeIsValid) { + return @[]; + } + + // Used for block measure option + __block CGRect firstRect = CGRectNull; + __block CGRect lastRect = CGRectNull; + __block CGRect blockRect = CGRectNull; + NSMutableArray *textRects = [NSMutableArray array]; + + NSString *string = _textStorage.string; + + NSRange totalGlyphRange = [_layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + + [_layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) { + + CGRect lineRect = CGRectNull; + // If we're empty, don't bother looping through glyphs, use the default. + if (CGRectIsEmpty(usedRect)) { + lineRect = usedRect; + } else { + // TextKit's bounding rect computations are just a touch off, so we actually + // compose the rects by hand from the center of the given TextKit bounds and + // imposing the font attributes returned by the glyph's font. + NSRange lineGlyphRange = NSIntersectionRange(totalGlyphRange, glyphRange); + for (NSUInteger i = lineGlyphRange.location; i < NSMaxRange(lineGlyphRange) && i < string.length; i++) { + // We grab the properly sized rect for the glyph + CGRect properGlyphRect = [self _rectForGlyphAtIndex:i measureOption:measureOption]; + + // Don't count empty glyphs towards our line rect. + if (!CGRectIsEmpty(properGlyphRect)) { + lineRect = CGRectIsNull(lineRect) ? properGlyphRect + : CGRectUnion(lineRect, properGlyphRect); + } + } + } + + if (!CGRectIsNull(lineRect)) { + if (measureOption == ASTextNodeRendererMeasureOptionBlock) { + // For the block measurement option we store the first & last rect as + // special cases, then merge everything else into a single block rect + if (CGRectIsNull(firstRect)) { + // We don't have a firstRect, so we must be on the first line. + firstRect = lineRect; + } else if(CGRectIsNull(lastRect)) { + // We don't have a lastRect, but we do have a firstRect, so we must + // be on the second line. No need to merge in the blockRect just yet + lastRect = lineRect; + } else if(CGRectIsNull(blockRect)) { + // We have both a first and last rect, so we must be on the third line + // we don't have any blockRect to merge it into, so we just set it + // directly. + blockRect = lastRect; + lastRect = lineRect; + } else { + // Everything is already set, so we just merge this line into the + // block. + blockRect = CGRectUnion(blockRect, lastRect); + lastRect = lineRect; + } + } else { + // If the block option isn't being used then each line is being treated + // individually. + [textRects addObject:[NSValue valueWithCGRect:lineRect]]; + } + } + }]; + + if (measureOption == ASTextNodeRendererMeasureOptionBlock) { + // Block measure option is handled differently with just 3 vars for the entire range. + if (!CGRectIsNull(firstRect)) { + if (!CGRectIsNull(blockRect)) { + CGFloat rightEdge = MAX(CGRectGetMaxX(blockRect), CGRectGetMaxX(lastRect)); + if (rightEdge > CGRectGetMaxX(firstRect)) { + // Force the right side of the first rect to properly align with the + // right side of the rightmost of the block and last rect + firstRect.size.width += rightEdge - CGRectGetMaxX(firstRect); + } + + // Force the left side of the block rect to properly align with the + // left side of the leftmost of the first and last rect + blockRect.origin.x = MIN(CGRectGetMinX(firstRect), CGRectGetMinX(lastRect)); + // Force the right side of the block rect to properly align with the + // right side of the rightmost of the first and last rect + blockRect.size.width += MAX(CGRectGetMaxX(firstRect), CGRectGetMaxX(lastRect)) - CGRectGetMaxX(blockRect); + } + if (!CGRectIsNull(lastRect)) { + // Force the left edge of the last rect to properly align with the + // left side of the leftmost of the first and block rect, if necessary. + CGFloat leftEdge = MIN(CGRectGetMinX(blockRect), CGRectGetMinX(firstRect)); + CGFloat lastRectNudgeAmount = MAX(CGRectGetMinX(lastRect) - leftEdge, 0); + lastRect.origin.x = MIN(leftEdge, CGRectGetMinX(lastRect)); + lastRect.size.width += lastRectNudgeAmount; + } + + [textRects addObject:[NSValue valueWithCGRect:firstRect]]; + } + if (!CGRectIsNull(blockRect)) { + [textRects addObject:[NSValue valueWithCGRect:blockRect]]; + } + if (!CGRectIsNull(lastRect)) { + [textRects addObject:[NSValue valueWithCGRect:lastRect]]; + } + } + + return textRects; +} + +- (CGRect)_rectForGlyphAtIndex:(NSUInteger)glyphIndex + measureOption:(ASTextNodeRendererMeasureOption)measureOption +{ + ASDN::MutexLocker l(_textKitLock); + + NSUInteger charIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + CGGlyph glyph = [_layoutManager glyphAtIndex:glyphIndex]; + CTFontRef font = (__bridge_retained CTFontRef)[_textStorage attribute:NSFontAttributeName + atIndex:charIndex + effectiveRange:NULL]; + if (font == nil) { + font = (__bridge_retained CTFontRef)[UIFont systemFontOfSize:12.0]; + } + + // Glyph Advance + // +-------------------------+ + // | | + // | | + // +------------------------+--|-------------------------|--+-----------+-----+ What TextKit returns sometimes + // | | | XXXXXXXXXXX + | | | (approx. correct height, but + // | ---------|--+---------+ XXX XXXX +|-----------|-----| sometimes inaccurate bounding + // | | | XXX XXXXX| | | widths) + // | | | XX XX | | | + // | | | XX | | | + // | | | XXX | | | + // | | | XX | | | + // | | | XXXXXXXXXXX | | | + // | Cap Height->| | XX | | | + // | | | XX | Ascent-->| | + // | | | XX | | | + // | | | XX | | | + // | | | X | | | + // | | | X | | | + // | | | X | | | + // | | | XX | | | + // | | | X | | | + // | ---------|-------+ X +-------------------------------------| + // | | XX | | + // | | X | | + // | | XX Descent------>| | + // | | XXXXXX | | + // | | XXX | | + // +------------------------+-------------------------------------------------+ + // | + // +--+Actual bounding box + + CGRect glyphRect = [_layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) + inTextContainer:_textContainer]; + + // If it is a NSTextAttachment, we don't have the matched glyph and use width of glyphRect instead of advance. + CGFloat advance = (glyph == kCGFontIndexInvalid) ? glyphRect.size.width : CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, NULL, 1); + + // We treat the center of the glyph's bounding box as the center of our new rect + CGPoint glyphCenter = CGPointMake(CGRectGetMidX(glyphRect), CGRectGetMidY(glyphRect)); + + CGRect properGlyphRect; + if (measureOption == ASTextNodeRendererMeasureOptionCapHeight + || measureOption == ASTextNodeRendererMeasureOptionBlock) { + CGFloat ascent = CTFontGetAscent(font); + CGFloat descent = CTFontGetDescent(font); + CGFloat capHeight = CTFontGetCapHeight(font); + CGFloat leading = CTFontGetLeading(font); + CGFloat glyphHeight = ascent + descent; + + // For visual balance, we add the cap height padding above the cap, and + // below the baseline, we scale by the descent so it grows with the size of + // the text. + CGFloat topPadding = ASTextNodeRendererTextCapHeightPadding * descent; + CGFloat bottomPadding = topPadding; + + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphCenter.y - glyphHeight * 0.5 + (ascent - capHeight) - topPadding + leading, + advance, + capHeight + topPadding + bottomPadding); + } else { + // We are just measuring the line heights here, so we can use the + // heights used by TextKit, which tend to be pretty good. + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphRect.origin.y, + advance, + glyphRect.size.height); + } + + CFRelease(font); + + return properGlyphRect; +} + +- (void)enumerateTextIndexesAtPosition:(CGPoint)position usingBlock:(as_renderer_index_block_t)block +{ + if (position.x > _constrainedSize.width + || position.y > _constrainedSize.height + || block == NULL) { + // Short circuit if the position is outside the size of this renderer, or + // if the block is null. + return; + } + + ASDN::MutexLocker l(_textKitLock); + + [self _initializeTextKitComponentsIfNeeded]; + + // We break it up into a 44pt box for the touch, and find the closest link + // attribute-containing glyph to the center of the touch. + CGFloat squareSide = 44.f; + // Should be odd if you want to test the center of the touch. + NSInteger pointsOnASide = 3; + + // The distance between any 2 of the adjacent points + CGFloat pointSeparation = squareSide / pointsOnASide; + // These are for tracking which point we're on. We start with -pointsOnASide/2 + // and go to pointsOnASide/2. So if pointsOnASide=3, we go from -1 to 1. + NSInteger endIndex = pointsOnASide / 2; + NSInteger startIndex = -endIndex; + + BOOL stop = NO; + for (NSInteger i = startIndex; i <= endIndex && !stop; i++) { + for (NSInteger j = startIndex; j <= endIndex && !stop; j++) { + CGPoint currentPoint = CGPointMake(position.x + i * pointSeparation, + position.y + j * pointSeparation); + + // We ask the layout manager for the proper glyph at the touch point + NSUInteger glyphIndex = [_layoutManager glyphIndexForPoint:currentPoint + inTextContainer:_textContainer]; + + // If it's an invalid glyph, quit. + BOOL isValidGlyph = NO; + [_layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph]; + if (!isValidGlyph) { + continue; + } + + NSUInteger characterIndex = [_layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + + CGRect glyphRect = [self _rectForGlyphAtIndex:glyphIndex + measureOption:ASTextNodeRendererMeasureOptionLineHeight]; + + // Sometimes TextKit plays jokes on us and returns glyphs that really + // aren't close to the point in question. Silly TextKit... + if (!CGRectContainsPoint(CGRectInset(glyphRect, -ASTextNodeRendererGlyphTouchHitSlop, -ASTextNodeRendererGlyphTouchHitSlop), currentPoint)) { + continue; + } + + block(characterIndex, glyphRect, &stop); + } + } +} + +#pragma mark - Truncation + +/* + * Calculates the intersection of the truncation message within the end of the + * last line. + * + * This is accomplished by temporarily adding an exclusion rect for the size of + * the truncation string at the end of the last line of text, and forcing the + * layout manager to re-layout and clip the text such that we get a natural + * clipping based on the settings of the layout manager. + */ +- (NSUInteger)_calculateCharacterIndexBeforeTruncationMessage +{ + ASDN::MutexLocker l(_textKitLock); + + CGRect constrainedRect = (CGRect){.size = _calculatedSize}; + + NSRange visibleGlyphRange = [_layoutManager glyphRangeForBoundingRect:constrainedRect inTextContainer:_textContainer]; + NSInteger lastVisibleGlyphIndex = (NSMaxRange(visibleGlyphRange) - 1); + CGRect lastLineRect = [_layoutManager lineFragmentRectForGlyphAtIndex:lastVisibleGlyphIndex effectiveRange:NULL]; + + // Calculate the bounding rectangle for the truncation message + ASTextKitComponents truncationComponents = ASTextKitComponentsCreate(_truncationString, constrainedRect.size); + + // Size the truncation message + [truncationComponents.layoutManager ensureLayoutForTextContainer:truncationComponents.textContainer]; + NSRange truncationGlyphRange = [truncationComponents.layoutManager glyphRangeForTextContainer:truncationComponents.textContainer]; + CGRect truncationUsedRect = [truncationComponents.layoutManager boundingRectForGlyphRange:truncationGlyphRange inTextContainer:truncationComponents.textContainer]; + CGRect translatedTruncationRect = CGRectMake(CGRectGetMaxX(constrainedRect) - truncationUsedRect.size.width, + CGRectGetMinY(lastLineRect), + truncationUsedRect.size.width, + truncationUsedRect.size.height); + + // Determine which glyph is the first to be clipped / overlaps the truncation message. + CGPoint beginningOfTruncationMessage = CGPointMake(translatedTruncationRect.origin.x, CGRectGetMidY(translatedTruncationRect)); + NSUInteger firstClippedGlyphIndex = [_layoutManager glyphIndexForPoint:beginningOfTruncationMessage inTextContainer:_textContainer fractionOfDistanceThroughGlyph:NULL]; + NSUInteger firstCharacterIndexToReplace = [_layoutManager characterIndexForGlyphAtIndex:firstClippedGlyphIndex]; + ASDisplayNodeAssert(firstCharacterIndexToReplace != NSNotFound, @"The beginning of the truncation message exclusion rect (%@) didn't intersect any glyphs", NSStringFromCGPoint(beginningOfTruncationMessage)); + + // Break on word boundaries + return [self _findTruncationInsertionPointAtOrBeforeCharacterIndex:firstCharacterIndexToReplace]; +} + ++ (NSCharacterSet *)_truncationCharacterSet +{ + static NSCharacterSet *truncationCharacterSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableCharacterSet *mutableCharacterSet = [[NSMutableCharacterSet alloc] init]; + [mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [mutableCharacterSet addCharactersInString:@".,!?:;"]; + truncationCharacterSet = mutableCharacterSet; + }); + return truncationCharacterSet; +} + +/** + * @abstract Finds the first whitespace at or before the character index do we don't truncate in the middle of words + * @discussion If there are multiple whitespaces together (say a space and a newline), this will backtrack to the first one + */ +- (NSUInteger)_findTruncationInsertionPointAtOrBeforeCharacterIndex:(NSUInteger)firstCharacterIndexToReplace +{ + ASDN::MutexLocker l(_textKitLock); + // Don't attempt to truncate beyond the beginning of the string + if (firstCharacterIndexToReplace >= _textStorage.length) { + return 0; + } + + // Find the glyph range of the line fragment containing the first character to replace. + NSRange lineGlyphRange; + [_layoutManager lineFragmentRectForGlyphAtIndex:[_layoutManager glyphIndexForCharacterAtIndex:firstCharacterIndexToReplace] + effectiveRange:&lineGlyphRange]; + + // Look for the first whitespace from the end of the line, starting from the truncation point + NSUInteger startingSearchIndex = [_layoutManager characterIndexForGlyphAtIndex:lineGlyphRange.location]; + NSUInteger endingSearchIndex = firstCharacterIndexToReplace; + NSRange rangeToSearch = NSMakeRange(startingSearchIndex, (endingSearchIndex - startingSearchIndex)); + + NSCharacterSet *truncationCharacterSet = [[self class] _truncationCharacterSet]; + + NSRange rangeOfLastVisibleWhitespace = [_textStorage.string rangeOfCharacterFromSet:truncationCharacterSet + options:NSBackwardsSearch + range:rangeToSearch]; + + // Couldn't find a good place to truncate. Might be because there is no whitespace in the text, or we're dealing + // with a foreign language encoding. Settle for truncating at the original place, which may be mid-word. + if (rangeOfLastVisibleWhitespace.location == NSNotFound) { + return firstCharacterIndexToReplace; + } else { + return rangeOfLastVisibleWhitespace.location; + } +} + +#pragma mark - Drawing + +- (void)drawInRect:(CGRect)bounds isRasterizing:(BOOL)isRasterizing +{ + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + + [self _initializeTextKitComponentsIfNeeded]; + NSRange glyphRange = [_layoutManager glyphRangeForTextContainer:_textContainer]; + { + ASDN::MutexLocker l(_textKitLock); + [_layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:bounds.origin]; + [_layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:bounds.origin]; + } + + CGContextRestoreGState(context); +} + +#pragma mark - String Ranges + +- (NSUInteger)lineCount +{ + ASDN::MutexLocker l(_textKitLock); + [self _initializeTextKitComponentsIfNeeded]; + + NSUInteger lineCount = 0; + for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [_layoutManager numberOfGlyphs]; lineCount++) { + [_layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; + } + return lineCount; +} + +- (NSRange)visibleRange +{ + ASDN::MutexLocker l(_textKitLock); + [self _initializeTextKitComponentsIfNeeded]; + return _visibleRange; +} + +- (NSRange)truncationStringCharacterRange +{ + ASDN::MutexLocker l(_textKitLock); + [self _initializeTextKitComponentsIfNeeded]; + return _truncationCharacterRange; +} + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeShadower.h b/AsyncDisplayKit/Details/ASTextNodeShadower.h new file mode 100644 index 0000000000..aea8394640 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeShadower.h @@ -0,0 +1,68 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +/** + * @abstract Negates/inverts a UIEdgeInsets. + * @discussion Useful for undoing the application of shadow padding to a frame/bounds CGRect. + * For example, + * CGRect insetRect = UIEdgeInsetsRect(originalRect, insets); + * CGRect equalToOriginalRect = UIEdgeInsetsRect(originalRect, ASDNEdgeInsetsInvert(insets)); + */ +static inline UIEdgeInsets ASDNEdgeInsetsInvert(UIEdgeInsets insets) +{ + UIEdgeInsets invertedInsets = UIEdgeInsetsMake(-insets.top, -insets.left, -insets.bottom, -insets.right); + return invertedInsets; +} + +/** + * @abstract an immutable class for calculating shadow padding drawing a shadowed background for text + */ +@interface ASTextNodeShadower : NSObject + +- (instancetype)initWithShadowOffset:(CGSize)shadowOffset + shadowColor:(CGColorRef)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius; + +/** + * @abstract The offset from the top-left corner at which the shadow starts. + * @discussion A positive width will move the shadow to the right. + * A positive height will move the shadow downwards. + */ +@property (nonatomic, assign, readonly) CGSize shadowOffset; + +//! CGColor in which the shadow is drawn +@property (nonatomic, assign, readonly) CGColorRef shadowColor; + +//! Alpha of the shadow +@property (nonatomic, assign, readonly) CGFloat shadowOpacity; + +//! Radius, in pixels +@property (nonatomic, assign, readonly) CGFloat shadowRadius; + +/** + * @abstract The edge insets which represent shadow padding + * @discussion Each edge inset is less than or equal to zero. + * + * Example: + * CGRect boundsWithoutShadowPadding; // Large enough to fit text, not large enough to fit the shadow as well + * UIEdgeInsets shadowPadding = [shadower shadowPadding]; + * CGRect boundsWithShadowPadding = UIEdgeInsetsRect(boundsWithoutShadowPadding, shadowPadding); + */ +- (UIEdgeInsets)shadowPadding; + +/** + * @abstract draws the shadow for text in the provided CGContext + * @discussion Call within the text node's +drawRect method + */ +- (void)setShadowInContext:(CGContextRef)context; + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeShadower.m b/AsyncDisplayKit/Details/ASTextNodeShadower.m new file mode 100644 index 0000000000..13eb27070c --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeShadower.m @@ -0,0 +1,90 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNodeShadower.h" + +@implementation ASTextNodeShadower { + UIEdgeInsets _calculatedShadowPadding; +} + +- (instancetype)initWithShadowOffset:(CGSize)shadowOffset + shadowColor:(CGColorRef)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius +{ + if (self = [super init]) { + _shadowOffset = shadowOffset; + _shadowColor = CGColorRetain(shadowColor); + _shadowOpacity = shadowOpacity; + _shadowRadius = shadowRadius; + _calculatedShadowPadding = UIEdgeInsetsMake(-INFINITY, -INFINITY, INFINITY, INFINITY); + } + return self; +} + +- (void)dealloc +{ + CGColorRelease(_shadowColor); +} + +/* + * This method is duplicated here because it gets called frequently, and we were + * wasting valuable time constructing a state object to ask it. + */ +- (BOOL)_shouldDrawShadow +{ + return _shadowOpacity != 0.0 && _shadowColor != NULL && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)); +} + +- (void)setShadowInContext:(CGContextRef)context +{ + if ([self _shouldDrawShadow]) { + CGColorRef textShadowColor = CGColorRetain(_shadowColor); + CGSize textShadowOffset = _shadowOffset; + CGFloat textShadowOpacity = _shadowOpacity; + CGFloat textShadowRadius = _shadowRadius; + + if (textShadowOpacity != 1.0) { + CGFloat inherentAlpha = CGColorGetAlpha(textShadowColor); + + CGColorRef oldTextShadowColor = textShadowColor; + textShadowColor = CGColorCreateCopyWithAlpha(textShadowColor, inherentAlpha * textShadowOpacity); + CGColorRelease(oldTextShadowColor); + } + + CGContextSetShadowWithColor(context, textShadowOffset, textShadowRadius, textShadowColor); + + CGColorRelease(textShadowColor); + } +} + + +- (UIEdgeInsets)shadowPadding +{ + if (_calculatedShadowPadding.top == -INFINITY) { + if (![self _shouldDrawShadow]) { + return UIEdgeInsetsZero; + } + + UIEdgeInsets shadowPadding = UIEdgeInsetsZero; + + // min values are expected to be negative for most typical shadowOffset and + // blurRadius settings: + shadowPadding.top = fminf(0.0f, _shadowOffset.height - _shadowRadius); + shadowPadding.left = fminf(0.0f, _shadowOffset.width - _shadowRadius); + + shadowPadding.bottom = fminf(0.0f, -_shadowOffset.height - _shadowRadius); + shadowPadding.right = fminf(0.0f, -_shadowOffset.width - _shadowRadius); + + _calculatedShadowPadding = shadowPadding; + } + + return _calculatedShadowPadding; +} + +@end diff --git a/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.h b/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.h new file mode 100644 index 0000000000..0adb7b437d --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.h @@ -0,0 +1,36 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +typedef struct { + NSTextStorage *textStorage; + NSTextContainer *textContainer; + NSLayoutManager *layoutManager; + UITextView *textView; +} ASTextKitComponents; + +// Convenience. +/** + @abstract Creates the stack of TextKit components. + @param attributedSeedString The attributed string to sed the returned text storage with, or nil to receive an blank text storage. + @param textContainerSize The size of the text-container. Typically, size specifies the constraining width of the layout, and FLT_MAX for height. Pass CGSizeZero if these components will be hooked up to a UITextView, which will manage the text container's size itself. + @return A {@ref ASTextKitComponents} containing the created components. The text view component will be nil. + @discussion The returned components will be hooked up together, so they are ready for use as a system upon return. + */ +extern ASTextKitComponents ASTextKitComponentsCreate(NSAttributedString *attributedSeedString, CGSize textContainerSize); +/** + @abstract Returns the bounding size for the text view's text. + @param components The TextKit components to calculate the constrained size of the text for. + @param constrainedWidth The constraining width to be used during text-sizing. Usually, this value should be the receiver's calculated size. + @result A CGSize representing the bounding size for the receiver's text. + */ +extern CGSize ASTextKitComponentsSizeForConstrainedWidth(ASTextKitComponents components, CGFloat constrainedWidth); diff --git a/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.mm b/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.mm new file mode 100644 index 0000000000..2ca5109a46 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeTextKitHelpers.mm @@ -0,0 +1,43 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNodeTextKitHelpers.h" + +#pragma mark - Convenience + +CGSize ASTextKitComponentsSizeForConstrainedWidth(ASTextKitComponents components, CGFloat constrainedWidth) +{ + // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. + // Otherwise, we create a temporary stack to size for `constrainedWidth`. + if (CGRectGetWidth(components.textView.bounds) != constrainedWidth) { + components = ASTextKitComponentsCreate(components.textStorage, CGSizeMake(constrainedWidth, FLT_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; +} + +ASTextKitComponents ASTextKitComponentsCreate(NSAttributedString *attributedSeedString, CGSize textContainerSize) +{ + ASTextKitComponents components; + + // Create the TextKit component stack with our default configuration. + components.textStorage = (attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init]); + + components.layoutManager = [[NSLayoutManager alloc] init]; + [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; +} diff --git a/AsyncDisplayKit/Details/ASTextNodeTypes.h b/AsyncDisplayKit/Details/ASTextNodeTypes.h new file mode 100644 index 0000000000..800f23ae26 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeTypes.h @@ -0,0 +1,12 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +// Use this attribute name to add "word kerning" +static NSString *const ASTextNodeWordKerningAttributeName = @"ASAttributedStringWordKerning"; diff --git a/AsyncDisplayKit/Details/ASTextNodeWordKerner.h b/AsyncDisplayKit/Details/ASTextNodeWordKerner.h new file mode 100644 index 0000000000..41c5b7a29f --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeWordKerner.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +/** + @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. + */ +@interface ASTextNodeWordKerner : NSObject + +/** + 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 diff --git a/AsyncDisplayKit/Details/ASTextNodeWordKerner.m b/AsyncDisplayKit/Details/ASTextNodeWordKerner.m new file mode 100644 index 0000000000..0fd7d25004 --- /dev/null +++ b/AsyncDisplayKit/Details/ASTextNodeWordKerner.m @@ -0,0 +1,129 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASTextNodeWordKerner.h" + +#import + +#import "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 NSControlCharacterWhitespaceAction; + + 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("com.facebook.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 diff --git a/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.h b/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.h new file mode 100644 index 0000000000..5754011c08 --- /dev/null +++ b/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.h @@ -0,0 +1,21 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface NSMutableAttributedString (TextKitAdditions) + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight maximumLineHeight:(CGFloat)maximumLineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitLineHeight:(CGFloat)lineHeight; + +- (void)attributeTextInRange:(NSRange)range withTextKitParagraphStyle:(NSParagraphStyle *)paragraphStyle; + +@end diff --git a/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.m b/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.m new file mode 100644 index 0000000000..87f8221978 --- /dev/null +++ b/AsyncDisplayKit/Details/NSMutableAttributedString+TextKitAdditions.m @@ -0,0 +1,48 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "NSMutableAttributedString+TextKitAdditions.h" + +@implementation NSMutableAttributedString (TextKitAdditions) + +#pragma mark - Convenience Methods + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight +{ + if (range.length) { + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + [style setMinimumLineHeight:minimumLineHeight]; + [self attributeTextInRange:range withTextKitParagraphStyle:style]; + } +} + +- (void)attributeTextInRange:(NSRange)range withTextKitMinimumLineHeight:(CGFloat)minimumLineHeight maximumLineHeight:(CGFloat)maximumLineHeight +{ + if (range.length) { + + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + [style setMinimumLineHeight:minimumLineHeight]; + [style setMaximumLineHeight:maximumLineHeight]; + [self attributeTextInRange:range withTextKitParagraphStyle:style]; + } +} + +- (void)attributeTextInRange:(NSRange)range withTextKitLineHeight:(CGFloat)lineHeight +{ + [self attributeTextInRange:range withTextKitMinimumLineHeight:lineHeight maximumLineHeight:lineHeight]; +} + +- (void)attributeTextInRange:(NSRange)range withTextKitParagraphStyle:(NSParagraphStyle *)paragraphStyle +{ + if (range.length) { + [self addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + } +} + +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h new file mode 100644 index 0000000000..2f2414d0b8 --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.h @@ -0,0 +1,138 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class _ASAsyncTransaction; + +typedef void(^asyncdisplaykit_async_transaction_completion_block_t)(_ASAsyncTransaction *completedTransaction, BOOL canceled); +typedef id(^asyncdisplaykit_async_transaction_operation_block_t)(void); +typedef void(^asyncdisplaykit_async_transaction_operation_completion_block_t)(id value, BOOL canceled); +typedef void(^asyncdisplaykit_async_transaction_complete_async_operation_block_t)(id value); +typedef void(^asyncdisplaykit_async_transaction_async_operation_block_t)(asyncdisplaykit_async_transaction_complete_async_operation_block_t completeOperationBlock); + +/** + State is initially ASAsyncTransactionStateOpen. + Every transaction MUST be committed. It is an error to fail to commit a transaction. + A committed transaction MAY be canceled. You cannot cancel an open (uncommitted) transaction. + */ +typedef NS_ENUM(NSUInteger, ASAsyncTransactionState) { + ASAsyncTransactionStateOpen = 0, + ASAsyncTransactionStateCommitted, + ASAsyncTransactionStateCanceled, +}; + +/** + @summary ASAsyncTransaction provides lightweight transaction semantics for asynchronous operations. + + @desc ASAsyncTransaction provides the following properties: + + - Transactions group an arbitrary number of operations, each consisting of an execution block and a completion block. + - The execution block returns a single object that will be passed to the completion block. + - Execution blocks added to a transaction will run in parallel on the global background dispatch queues; + the completion blocks are dispatched to the callback queue. + - Every operation completion block is guaranteed to execute, regardless of cancelation. + However, execution blocks may be skipped if the transaction is canceled. + - Operation completion blocks are always executed in the order they were added to the transaction, assuming the + callback queue is serial of course. + */ +@interface _ASAsyncTransaction : NSObject + +/** + @summary Initialize a transaction that can start collecting async operations. + + @see initWithCallbackQueue:commitBlock:completionBlock:executeConcurrently: + @param callbackQueue The dispatch queue that the completion blocks will be called on. + @param completionBlock A block that is called when the transaction is completed. May be NULL. + */ +- (id)initWithCallbackQueue:(dispatch_queue_t)callbackQueue + completionBlock:(asyncdisplaykit_async_transaction_completion_block_t)completionBlock; + +/** + The dispatch queue that the completion blocks will be called on. + */ +@property (nonatomic, retain, readonly) dispatch_queue_t callbackQueue; + +/** + A block that is called when the transaction is completed. + */ +@property (nonatomic, copy, readonly) asyncdisplaykit_async_transaction_completion_block_t completionBlock; + +/** + The state of the transaction. + @see ASAsyncTransactionState + */ +@property (nonatomic, readonly) ASAsyncTransactionState state; + +/** + @summary Adds a synchronous operation to the transaction. The execution block will be executed immediately. + + @desc The block will be executed on the specified queue and is expected to complete synchronously. The async + transaction will wait for all operations to execute on their appropriate queues, so the blocks may still be executing + async if they are running on a concurrent queue, even though the work for this block is synchronous. + + @param block The execution block that will be executed on a background queue. This is where the expensive work goes. + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; + +/** + @summary Adds an async operation to the transaction. The execution block will be executed immediately. + + @desc The block will be executed on the specified queue and is expected to complete asynchronously. The block will be + supplied with a completion block that can be executed once its async operation is completed. This is useful for + network downloads and other operations that have an async API. + + WARNING: Consumers MUST call the completeOperationBlock passed into the work block, or objects will be leaked! + + @param block The execution block that will be executed on a background queue. This is where the expensive work goes. + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ +- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion; + + +/** + @summary Adds a block to run on the completion of the async transaction. + + @param queue The dispatch queue on which to execute the block. + @param completion The completion block that will be executed with the output of the execution block when all of the + operations in the transaction are completed. Executed and released on callbackQueue. + */ + +- (void)addCompletionBlock:(asyncdisplaykit_async_transaction_completion_block_t)completion; + +/** + @summary Cancels all operations in the transaction. + + @desc You can only cancel a commmitted transaction. + + All completion blocks are always called, regardless of cancelation. Execution blocks may be skipped if canceled. + */ +- (void)cancel; + +/** + @summary Marks the end of adding operations to the transaction. + + @desc You MUST commit every transaction you create. It is an error to create a transaction that is never committed. + + When all of the operations that have been added have completed the transaction will execute their completion + blocks. + + If no operations were added to this transaction, invoking commit will execute the transaction's completion block synchronously. + */ +- (void)commit; + +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.m b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.m new file mode 100644 index 0000000000..0861d09722 --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransaction.m @@ -0,0 +1,173 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASAsyncTransaction.h" + +#import "ASAssert.h" + +@interface ASDisplayNodeAsyncTransactionOperation : NSObject +- (id)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock; +@property (nonatomic, copy) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock; +@property (atomic, retain) id value; // set on bg queue by the operation block +@end + +@implementation ASDisplayNodeAsyncTransactionOperation + +- (id)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock +{ + if ((self = [super init])) { + _operationCompletionBlock = [operationCompletionBlock copy]; + } + return self; +} + +- (void)dealloc +{ + ASDisplayNodeAssertNil(_operationCompletionBlock, @"Should have been called and released before -dealloc"); +} + +- (void)callAndReleaseCompletionBlock:(BOOL)canceled; +{ + if (_operationCompletionBlock) { + _operationCompletionBlock(self.value, canceled); + // Guarantee that _operationCompletionBlock is released on _callbackQueue: + self.operationCompletionBlock = nil; + } +} + +@end + +@implementation _ASAsyncTransaction +{ + dispatch_group_t _group; + NSMutableArray *_operations; +} + +#pragma mark - +#pragma mark Lifecycle + +- (id)initWithCallbackQueue:(dispatch_queue_t)callbackQueue + completionBlock:(void(^)(_ASAsyncTransaction *, BOOL))completionBlock +{ + if ((self = [self init])) { + if (callbackQueue == NULL) { + callbackQueue = dispatch_get_main_queue(); + } + _callbackQueue = callbackQueue; + _completionBlock = [completionBlock copy]; + + _state = ASAsyncTransactionStateOpen; + } + return self; +} + +- (void)dealloc +{ + // Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue. + ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed"); +} + +#pragma mark - +#pragma mark Transaction Management + +- (void)addAsyncOperationWithBlock:(asyncdisplaykit_async_transaction_async_operation_block_t)block + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + + [self _ensureTransactionData]; + + ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + dispatch_group_async(_group, queue, ^{ + if (_state != ASAsyncTransactionStateCanceled) { + dispatch_group_enter(_group); + block(^(id value){ + operation.value = value; + dispatch_group_leave(_group); + }); + } + }); +} + +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + + [self _ensureTransactionData]; + + ASDisplayNodeAsyncTransactionOperation *operation = [[ASDisplayNodeAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + dispatch_group_async(_group, queue, ^{ + if (_state != ASAsyncTransactionStateCanceled) { + operation.value = block(); + } + }); +} + +- (void)addCompletionBlock:(asyncdisplaykit_async_transaction_completion_block_t)completion +{ + __weak typeof(self) weakSelf = self; + [self addOperationWithBlock:^(){return (id)nil;} queue:_callbackQueue completion:^(id value, BOOL canceled) { + typeof(self) strongSelf = weakSelf; + completion(strongSelf, canceled); + }]; +} + +- (void)cancel +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction"); + _state = ASAsyncTransactionStateCanceled; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction"); + _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 { + ASDisplayNodeAssert(_group != NULL, @"If there are operations, dispatch group should have been created"); + dispatch_group_notify(_group, _callbackQueue, ^{ + BOOL isCanceled = (_state == ASAsyncTransactionStateCanceled); + for (ASDisplayNodeAsyncTransactionOperation *operation in _operations) { + [operation callAndReleaseCompletionBlock:isCanceled]; + } + if (_completionBlock) { + _completionBlock(self, isCanceled); + } + }); + } +} + +#pragma mark - +#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 = dispatch_group_create(); + } + if (_operations == nil) { + _operations = [[NSMutableArray alloc] init]; + } +} + +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer+Private.h b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer+Private.h new file mode 100644 index 0000000000..33f4950a09 --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer+Private.h @@ -0,0 +1,17 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASAsyncTransactionContainer.h" + +@interface CALayer (ASAsyncTransactionContainerTransactions) +@property (nonatomic, retain, setter = asyncdisplaykit_setAsyncLayerTransactions:) NSHashTable *asyncdisplaykit_asyncLayerTransactions; +@property (nonatomic, retain, setter = asyncdisplaykit_setCurrentAsyncLayerTransaction:) _ASAsyncTransaction *asyncdisplaykit_currentAsyncLayerTransaction; + +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction; +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction; +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.h b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.h new file mode 100644 index 0000000000..92479edc9e --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.h @@ -0,0 +1,71 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@class _ASAsyncTransaction; + +typedef NS_ENUM(NSUInteger, ASAsyncTransactionContainerState) { + /** + The async container has no outstanding transactions. + Whatever it is displaying is up-to-date. + */ + ASAsyncTransactionContainerStateNoTransactions = 0, + /** + The async container has one or more outstanding async transactions. + Its contents may be out of date or showing a placeholder, depending on the configuration of the contained ASDisplayLayers. + */ + ASAsyncTransactionContainerStatePendingTransactions, +}; + +@protocol ASDisplayNodeAsyncTransactionContainer + +/** + @summary If YES, the receiver is marked as a container for async display, grouping all of the async display calls + in the layer hierarchy below the receiver together in a single ASAsyncTransaction. + + @default NO + */ +@property (nonatomic, assign, getter = asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; + +/** + @summary The current state of the receiver; indicates if it is currently performing asynchronous operations or if all operations have finished/canceled. + */ +@property (nonatomic, assign, readonly) ASAsyncTransactionContainerState asyncdisplaykit_asyncTransactionContainerState; + +/** + @summary Cancels all async transactions on the receiver. + */ +- (void)asyncdisplaykit_cancelAsyncTransactions; + +/** + @summary Invoked when the asyncdisplaykit_asyncTransactionContainerState property changes. + @desc You may want to override this in a CALayer or UIView subclass to take appropriate action (such as hiding content while it renders). + */ +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange; + +@end + +@interface CALayer (ASDisplayNodeAsyncTransactionContainer) +/** + @summary Returns the current async transaction for this container layer. A new transaction is created if one + did not already exist. This method will always return an open, uncommitted transaction. + @desc asyncdisplaykit_isAsyncTransactionContainer does not need to be YES for this to return a transaction. + */ +@property (nonatomic, retain, readonly) _ASAsyncTransaction *asyncdisplaykit_asyncTransaction; + +/** + @summary Goes up the superlayer chain until it finds the first layer with asyncdisplaykit_isAsyncTransactionContainer=YES (including the receiver) and returns it. + Returns nil if no parent container is found. + */ +@property (nonatomic, retain, readonly) CALayer *asyncdisplaykit_parentTransactionContainer; +@end + +@interface UIView (ASDisplayNodeAsyncTransactionContainer) +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.m b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.m new file mode 100644 index 0000000000..cbf26a6de3 --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionContainer.m @@ -0,0 +1,119 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASAsyncTransactionContainer+Private.h" + +#import "_ASAsyncTransaction.h" +#import "_ASAsyncTransactionGroup.h" + +@implementation CALayer (ASAsyncTransactionContainerTransactions) +@dynamic asyncdisplaykit_asyncLayerTransactions; +@dynamic asyncdisplaykit_currentAsyncLayerTransaction; + +// No-ops in the base class. Mostly exposed for testing. +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction {} +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction {} +@end + +@implementation CALayer (ASDisplayNodeAsyncTransactionContainer) + +@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_currentAsyncLayerTransaction; + [currentTransaction commit]; + self.asyncdisplaykit_currentAsyncLayerTransaction = nil; + + for (_ASAsyncTransaction *transaction in [self.asyncdisplaykit_asyncLayerTransactions copy]) { + [transaction cancel]; + } +} + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(asyncdisplaykit_asyncTransactionContainerStateDidChange)]) { + [delegate asyncdisplaykit_asyncTransactionContainerStateDidChange]; + } +} + +- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction +{ + _ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncLayerTransaction; + if (transaction == nil) { + NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions; + if (transactions == nil) { + transactions = [NSHashTable hashTableWithOptions:NSPointerFunctionsObjectPointerPersonality]; + self.asyncdisplaykit_asyncLayerTransactions = transactions; + } + transaction = [[_ASAsyncTransaction alloc] initWithCallbackQueue:dispatch_get_main_queue() completionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) { + [transactions removeObject:completedTransaction]; + [self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction]; + if ([transactions count] == 0) { + [self asyncdisplaykit_asyncTransactionContainerStateDidChange]; + } + }]; + [transactions addObject:transaction]; + self.asyncdisplaykit_currentAsyncLayerTransaction = transaction; + [self asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:transaction]; + if ([transactions count] == 1) { + [self asyncdisplaykit_asyncTransactionContainerStateDidChange]; + } + } + [[_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 (ASDisplayNodeAsyncTransactionContainer) + +- (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. +} + +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.h b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.h new file mode 100644 index 0000000000..414465f0da --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.h @@ -0,0 +1,22 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class _ASAsyncTransaction; + +/// A group of transaction container layers, for which the current transactions are committed together at the end of the next runloop tick. +@interface _ASAsyncTransactionGroup : NSObject +/// The main transaction group is scheduled to commit on every tick of the main runloop. ++ (instancetype)mainTransactionGroup; + +/// Add a transaction container to be committed. +/// @param containerLayer A layer containing a transaction to be commited. May or may not be a container layer. +/// @see ASAsyncTransactionContainer +- (void)addTransactionContainer:(CALayer *)containerLayer; +@end diff --git a/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.m b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.m new file mode 100644 index 0000000000..c78a42d047 --- /dev/null +++ b/AsyncDisplayKit/Details/Transactions/_ASAsyncTransactionGroup.m @@ -0,0 +1,105 @@ + /* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASAssert.h" + +#import "_ASAsyncTransaction.h" +#import "_ASAsyncTransactionGroup.h" +#import "_ASAsyncTransactionContainer+Private.h" + +static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info); + +@interface _ASAsyncTransactionGroup () ++ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup; +- (void)commit; +@end + +@implementation _ASAsyncTransactionGroup { + NSHashTable *_containerLayers; +} + ++ (_ASAsyncTransactionGroup *)mainTransactionGroup +{ + ASDisplayNodeAssertMainThread(); + static _ASAsyncTransactionGroup *mainTransactionGroup; + + if (mainTransactionGroup == nil) { + mainTransactionGroup = [[_ASAsyncTransactionGroup alloc] init]; + [self registerTransactionGroupAsMainRunloopObserver:mainTransactionGroup]; + } + return mainTransactionGroup; +} + ++ (void)registerTransactionGroupAsMainRunloopObserver:(_ASAsyncTransactionGroup *)transactionGroup +{ + 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 + CFRunLoopObserverContext context = { + 0, // version + (__bridge void *)transactionGroup, // info + &CFRetain, // retain + &CFRelease, // release + NULL // copyDescription + }; + + observer = CFRunLoopObserverCreate(NULL, // allocator + activities, // activities + YES, // repeats + INT_MAX, // order after CA transaction commits + &_transactionGroupRunLoopObserverCallback, // callback + &context); // context + CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes); + CFRelease(observer); +} + +- (id)init +{ + if ((self = [super init])) { + _containerLayers = [NSHashTable hashTableWithOptions:NSPointerFunctionsObjectPointerPersonality]; + } + return self; +} + +- (void)addTransactionContainer:(CALayer *)containerLayer +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(containerLayer != nil, @"No container"); + [_containerLayers addObject:containerLayer]; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + + if ([_containerLayers count]) { + NSHashTable *containerLayersToCommit = [_containerLayers copy]; + [_containerLayers removeAllObjects]; + + for (CALayer *containerLayer in containerLayersToCommit) { + // 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 = containerLayer.asyncdisplaykit_currentAsyncLayerTransaction; + containerLayer.asyncdisplaykit_currentAsyncLayerTransaction = nil; + [transaction commit]; + } + } +} + +@end + +static void _transactionGroupRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) +{ + ASDisplayNodeCAssertMainThread(); + _ASAsyncTransactionGroup *group = (__bridge _ASAsyncTransactionGroup *)info; + [group commit]; +} diff --git a/AsyncDisplayKit/Details/UIView+ASConvenience.h b/AsyncDisplayKit/Details/UIView+ASConvenience.h new file mode 100644 index 0000000000..69997794c8 --- /dev/null +++ b/AsyncDisplayKit/Details/UIView+ASConvenience.h @@ -0,0 +1,79 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + These are the properties we support from CALayer (implemented in the pending state) + */ + +@protocol ASDisplayProperties + +@property (nonatomic, assign) CGPoint position; +@property (nonatomic, assign) CGFloat zPosition; +@property (nonatomic, assign) CGPoint anchorPoint; +@property (nonatomic, retain) id contents; +@property (nonatomic, assign) CGFloat contentsScale; +@property (nonatomic, assign) CATransform3D transform; +@property (nonatomic, assign) CATransform3D sublayerTransform; +@property (nonatomic, assign) BOOL needsDisplayOnBoundsChange; +@property (nonatomic, retain) __attribute__((NSObject)) CGColorRef shadowColor; +@property (nonatomic, assign) CGFloat shadowOpacity; +@property (nonatomic, assign) CGSize shadowOffset; +@property (nonatomic, assign) CGFloat shadowRadius; +@property (nonatomic, assign) CGFloat borderWidth; +@property (nonatomic, assign, getter = isOpaque) BOOL opaque; +@property (nonatomic, retain) __attribute__((NSObject)) CGColorRef borderColor; +@property (nonatomic, copy) NSString *asyncdisplaykit_name; +@property (nonatomic, retain) __attribute__((NSObject)) CGColorRef backgroundColor; +@property (nonatomic, assign) BOOL allowsEdgeAntialiasing; +@property (nonatomic, assign) unsigned int edgeAntialiasingMask; + +- (void)setNeedsDisplay; +- (void)setNeedsLayout; + +@end + +/** + These are all of the "good" properties of the UIView API that we support in pendingViewState or view of an ASDisplayNode. + */ +@protocol ASDisplayNodeViewProperties + +@property (nonatomic, assign) BOOL clipsToBounds; +@property (nonatomic, getter=isHidden) BOOL hidden; +@property (nonatomic, assign) BOOL autoresizesSubviews; +@property (nonatomic, assign) UIViewAutoresizing autoresizingMask; +@property (nonatomic, assign) CGFloat alpha; +@property (nonatomic, assign) CGRect bounds; +@property (nonatomic, assign) UIViewContentMode contentMode; +@property (nonatomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; +@property (nonatomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch; +@property (nonatomic, assign, getter = asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer; + +/** + Following properties of the UIAccessibility informal protocol are supported as well. + We don't declare them here, so _ASPendingState does not complain about them being not implemented, + as they are already on NSObject + + @property (atomic, assign) BOOL isAccessibilityElement; + @property (atomic, copy) NSString *accessibilityLabel; + @property (atomic, copy) NSString *accessibilityHint; + @property (atomic, copy) NSString *accessibilityValue; + @property (atomic, assign) UIAccessibilityTraits accessibilityTraits; + @property (atomic, assign) CGRect accessibilityFrame; + @property (atomic, retain) NSString *accessibilityLanguage; + @property (atomic, assign) BOOL accessibilityElementsHidden; + @property (atomic, assign) BOOL accessibilityViewIsModal; + @property (atomic, assign) BOOL shouldGroupAccessibilityChildren; + */ + +@end + +@interface CALayer (ASDisplayNodeLayer) +@property (atomic, copy) NSString *asyncdisplaykit_name; +@end diff --git a/AsyncDisplayKit/Details/UIView+ASConvenience.m b/AsyncDisplayKit/Details/UIView+ASConvenience.m new file mode 100644 index 0000000000..6772024821 --- /dev/null +++ b/AsyncDisplayKit/Details/UIView+ASConvenience.m @@ -0,0 +1,13 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "UIView+ASConvenience.h" + +@implementation CALayer (ASDisplayNodeLayer) +@dynamic asyncdisplaykit_name; +@end diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.h b/AsyncDisplayKit/Details/_ASDisplayLayer.h new file mode 100644 index 0000000000..cf45f4e5d2 --- /dev/null +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.h @@ -0,0 +1,123 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class ASSentinel; +@protocol _ASDisplayLayerDelegate; + +// Type for the cancellation checker block passed into the async display blocks. YES means the operation has been cancelled, NO means continue. +typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); + +@interface _ASDisplayLayer : CALayer + +/** + @summary Set to YES to enable asynchronous display for the receiver. + + @default YES (note that this might change for subclasses) + */ +@property (atomic, assign) BOOL displaysAsynchronously; + +/** + @summary Cancels any pending async display. + + @desc If the receiver has had display called and is waiting for the dispatched async display to be executed, this will + cancel that dispatched async display. This method is useful to call when removing the receiver from the window. + */ +- (void)cancelAsyncDisplay; + +/** + @summary The dispatch queue used for async display. + + @desc This is exposed here for tests only. + */ ++ (dispatch_queue_t)displayQueue; + +@property (nonatomic, strong, readonly) ASSentinel *displaySentinel; + +/** + @summary Delegate for asynchronous display of the layer. + + @desc The asyncDelegate will have the opportunity to override the methods related to async display. + */ +@property (atomic, weak) id<_ASDisplayLayerDelegate> asyncDelegate; + +/** + @summary Suspends both asynchronous and synchronous display of the receiver if YES. + + @desc This can be used to suspend all display calls while the receiver is still in the view hierarchy. If you + want to just cancel pending async display, use cancelAsyncDisplay instead. + + @default NO + */ +@property (atomic, assign, getter = isDisplaySuspended) BOOL displaySuspended; + +/** + @summary Bypasses asynchronous rendering and performs a blocking display immediately on the current thread. + + @desc Used by ASDisplayNode to display the layer synchronously on-demand (must be called on the main thread). + */ +- (void)displayImmediately; + +@end + +/** + Implement one of +displayAsyncLayer:parameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide drawing for your node. + Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for use on the display queue. + display/drawRect implementations MUST be thread-safe, as they can be called on the displayQueue (async) or the main thread (sync/displayImmediately) + */ +@protocol _ASDisplayLayerDelegate + +@optional + +// Called on the display queue and/or main queue (MUST BE THREAD SAFE) + +/** + @summary Delegate method to draw layer contents into a CGBitmapContext. The current UIGraphics context will be set to an appropriate context. + @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: + @param isCancelled Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. + @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store. + */ ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; + +/** + @summary Delegate override to provide new layer contents as a UIImage. + @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: + @param isCancelled Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. + @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. + */ ++ (UIImage *)displayWithParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock; + +// Called on the main thread only + +/** + @summary Delegate override for drawParameters + */ +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer; + +/** + @summary Delegate override for willDisplay + */ +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer; + +/** + @summary Delegate override for didDisplay + */ +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer; + +/** + @summary Delegate callback to display a layer, synchronously or asynchronously. 'asyncLayer' does not necessarily need to exist (can be nil). Typically, a delegate will display/draw its own contents and then set .contents on the layer when finished. + */ +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously; + +/** + @summary Delegate callback to handle a layer which requests its asynchronous display be cancelled. + */ +- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer; + +@end diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.mm b/AsyncDisplayKit/Details/_ASDisplayLayer.mm new file mode 100644 index 0000000000..9240c0a42a --- /dev/null +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.mm @@ -0,0 +1,216 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASDisplayLayer.h" + +#import + +#import "_ASAsyncTransactionContainer.h" +#import "ASAssert.h" +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" + +@implementation _ASDisplayLayer +{ + ASDN::Mutex _asyncDelegateLock; + // We can take this lock when we're setting displaySuspended and in setNeedsDisplay, so to not deadlock, this is recursive + ASDN::RecursiveMutex _displaySuspendedLock; + BOOL _displaySuspended; + + id<_ASDisplayLayerDelegate> __weak _asyncDelegate; +} + +@dynamic displaysAsynchronously; + +#pragma mark - +#pragma mark Lifecycle + +- (id)init +{ + if ((self = [super init])) { + _displaySentinel = [[ASSentinel alloc] init]; + + self.opaque = YES; + +#if DEBUG + // This is too expensive to do in production on all layers. + self.name = [NSString stringWithFormat:@"%@ (%p)", NSStringFromClass([self class]), self]; +#endif + } + return self; +} + +#pragma mark - +#pragma mark Properties + +- (id<_ASDisplayLayerDelegate>)asyncDelegate +{ + ASDN::MutexLocker l(_asyncDelegateLock); + return _asyncDelegate; +} + +- (void)setAsyncDelegate:(id<_ASDisplayLayerDelegate>)asyncDelegate +{ + ASDisplayNodeAssert(!asyncDelegate || [asyncDelegate isKindOfClass:[ASDisplayNode class]], @"_ASDisplayLayer is inherently coupled to ASDisplayNode and cannot be used with another asyncDelegate. Please rethink what you are trying to do."); + ASDN::MutexLocker l(_asyncDelegateLock); + _asyncDelegate = asyncDelegate; +} + +- (void)setContents:(id)contents +{ + ASDisplayNodeAssertMainThread(); + [super setContents:contents]; +} + +- (BOOL)isDisplaySuspended +{ + ASDN::MutexLocker l(_displaySuspendedLock); + return _displaySuspended; +} + +- (void)setDisplaySuspended:(BOOL)displaySuspended +{ + ASDN::MutexLocker l(_displaySuspendedLock); + 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)layoutSublayers +{ + [super layoutSublayers]; + + ASDisplayNode *node = self.asyncdisplaykit_node; + // If our associated node is layer-backed, we cannot rely on the view's -layoutSubviews calling the node's -layout implementation, so do it ourselves. + if (node.isLayerBacked) { + ASDisplayNodeAssertMainThread(); + [node __layout]; + } +} + +- (void)setNeedsLayout +{ + ASDisplayNodeAssertMainThread(); + [super setNeedsLayout]; +} + +- (void)setNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + + ASDN::MutexLocker l(_displaySuspendedLock); + [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("com.facebook.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 { + return [super defaultValueForKey:key]; + } +} + +#pragma mark - +#pragma mark Display + +- (void)displayImmediately +{ + // REVIEW: Should this respect isDisplaySuspended? If so, we'd probably want to synchronously display when + // setDisplaySuspended:No is called, rather than just scheduling. The thread affinity for the displayImmediately + // call will be tricky if we need to support this, though. It probably should just execute if displayImmediately is + // called directly. The caller should be responsible for not calling displayImmediately if it wants to obey the + // suspended state. + + 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 +{ + [self _hackResetNeedsDisplay]; + + ASDisplayNodeAssertMainThread(); + if (self.isDisplaySuspended) { + return; + } + + [self display:self.displaysAsynchronously]; +} + +- (void)display:(BOOL)asynchronously +{ + [self _performBlockWithAsyncDelegate:^(id<_ASDisplayLayerDelegate> asyncDelegate) { + [asyncDelegate displayAsyncLayer:self asynchronously:asynchronously]; + }]; +} + +- (void)cancelAsyncDisplay +{ + ASDisplayNodeAssertMainThread(); + [_displaySentinel increment]; + [self _performBlockWithAsyncDelegate:^(id<_ASDisplayLayerDelegate> asyncDelegate) { + [asyncDelegate cancelDisplayAsyncLayer:self]; + }]; +} + +- (NSString *)description +{ + // The standard UIView description is useless for debugging because all ASDisplayNode subclasses have _ASDisplayView-type views. + // This allows us to at least see the name of the node subclass and get its pointer directly from [[UIWindow keyWindow] recursiveDescription]. + return [NSString stringWithFormat:@"<%@, layer = %@>", self.asyncdisplaykit_node, [super description]]; +} + +#pragma mark - +#pragma mark Helper Methods + +- (void)_performBlockWithAsyncDelegate:(void(^)(id<_ASDisplayLayerDelegate> asyncDelegate))block +{ + id<_ASDisplayLayerDelegate> __attribute__((objc_precise_lifetime)) strongAsyncDelegate; + { + ASDN::MutexLocker l(_asyncDelegateLock); + strongAsyncDelegate = _asyncDelegate; + } + block(strongAsyncDelegate); +} + +@end diff --git a/AsyncDisplayKit/Details/_ASDisplayView.h b/AsyncDisplayKit/Details/_ASDisplayView.h new file mode 100644 index 0000000000..b6adce64c8 --- /dev/null +++ b/AsyncDisplayKit/Details/_ASDisplayView.h @@ -0,0 +1,16 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +// This class is only for use by ASDisplayNode and should never be subclassed or used directly. +// Note that the "node" property is added to UIView directly via a category in ASDisplayNode. + +@interface _ASDisplayView : UIView + +@end diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm new file mode 100644 index 0000000000..4314cc694b --- /dev/null +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -0,0 +1,212 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASDisplayView.h" + +#import + +#import "_ASCoreAnimationExtras.h" +#import "_ASAsyncTransactionContainer.h" +#import "ASAssert.h" +#import "ASDisplayNodeExtras.h" +#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+Subclasses.h" + +@interface _ASDisplayView () +@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; +@end + +@implementation _ASDisplayView +{ + ASDisplayNode *_node; // Though UIView has a .node property added via category, since we can add an ivar to a subclass, use that for performance. + BOOL _inHitTest; + BOOL _inPointInside; +} + +@synthesize asyncdisplaykit_node = _node; + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - NSObject Overrides +- (id)init +{ + return [self initWithFrame:CGRectZero]; +} + +- (NSString *)description +{ + // The standard UIView description is useless for debugging because all ASDisplayNode subclasses have _ASDisplayView-type views. + // This allows us to at least see the name of the node subclass and get its pointer directly from [[UIWindow keyWindow] recursiveDescription]. + return [NSString stringWithFormat:@"<%@, view = %@>", _node, [super description]]; +} + +#pragma mark - UIView Overrides + +- (id)initWithFrame:(CGRect)frame +{ + if (!(self = [super initWithFrame:frame])) + return nil; + + return self; +} + +- (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). + UIView *currentSuperview = self.superview; + if (!currentSuperview && newSuperview) { + [_node retain]; + } + else if (currentSuperview && !newSuperview) { + [_node release]; + } +} + +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + BOOL visible = newWindow != nil; + if (visible && !_node.inWindow) { + [_node __appear]; + } else if (!visible && _node.inWindow) { + [_node __disappear]; + } +} + +- (void)didMoveToSuperview +{ + // FIXME maybe move this logic into ASDisplayNode addSubnode/removeFromSupernode + UIView *superview = self.superview; + + // If superview's node is different from supernode's view, fix it by setting supernode to the new superview's node. Got that? + if (!superview) + [_node __setSupernode:nil]; + else if (superview != _node.supernode.view) + [_node __setSupernode:superview.asyncdisplaykit_node]; +} + +- (void)setNeedsDisplay +{ + // Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:. + if (ASDisplayNodeThreadIsMain()) { + [self.layer setNeedsDisplay]; + } else { + dispatch_async(dispatch_get_main_queue(), ^ { + [self.layer setNeedsDisplay]; + }); + } +} + +- (void)setNeedsLayout +{ + if (ASDisplayNodeThreadIsMain()) { + [super setNeedsLayout]; + } else { + dispatch_async(dispatch_get_main_queue(), ^ { + [super setNeedsLayout]; + }); + } +} + +- (void)layoutSubviews +{ + if (ASDisplayNodeThreadIsMain()) { + [_node __layout]; + } else { + // FIXME: CRASH This should not be happening because of the way we gate -setNeedsLayout, but it has been seen. + ASDisplayNodeFailAssert(@"not reached assertion"); + dispatch_async(dispatch_get_main_queue(), ^ { + [_node __layout]; + }); + } +} + +- (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; +} + +#pragma mark - Event Handling + UIResponder Overrides +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [_node touchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [_node touchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [_node touchesEnded:touches withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [_node 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: + 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. + if (!_inPointInside) { + _inPointInside = YES; + BOOL result = [_node pointInside:point withEvent:event]; + _inPointInside = NO; + return result; + } else { + return [super pointInside:point withEvent:event]; + } +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_6_0 +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + return [_node gestureRecognizerShouldBegin:gestureRecognizer]; +} +#endif + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ + [_node asyncdisplaykit_asyncTransactionContainerStateDidChange]; +} + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm new file mode 100644 index 0000000000..7b14fe92ed --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -0,0 +1,336 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASCoreAnimationExtras.h" +#import "_ASAsyncTransaction.h" +#import "ASAssert.h" +#import "ASDisplayNodeInternal.h" + +@implementation ASDisplayNode (AsyncDisplay) + +/** + * Support for limiting the number of concurrent displays. + * Set __ASDisplayLayerMaxConcurrentDisplayCount to change the maximum allowed number of concurrent displays. + */ + +#define ASDISPLAYNODE_DELAY_DISPLAY 0 + +#if ASDISPLAYNODE_DELAY_DISPLAY +static long __ASDisplayLayerMaxConcurrentDisplayCount = 1; +#else +static long __ASDisplayLayerMaxConcurrentDisplayCount = 1024; // essentially no limit until we determine a good value. +#endif +static dispatch_semaphore_t __ASDisplayLayerConcurrentDisplaySemaphore; + +/* + * Call __ASDisplayLayerIncrementConcurrentDisplayCount() upon entry into a display block (either drawRect: or display). + * This will block if the number of currently executing displays is equal or greater to the limit. + */ +static void __ASDisplayLayerIncrementConcurrentDisplayCount(BOOL displayIsAsync, BOOL isRasterizing) +{ + // Displays while rasterizing are not counted as concurrent displays, because they draw in serial when their rasterizing container displays. + if (isRasterizing) { + return; + } + + static dispatch_once_t onceToken; + if (displayIsAsync) { + dispatch_once(&onceToken, ^{ + __ASDisplayLayerConcurrentDisplaySemaphore = dispatch_semaphore_create(__ASDisplayLayerMaxConcurrentDisplayCount); + }); + + dispatch_semaphore_wait(__ASDisplayLayerConcurrentDisplaySemaphore, DISPATCH_TIME_FOREVER); + } + +#if ASDISPLAYNODE_DELAY_DISPLAY + usleep( (long)(0.05 * USEC_PER_SEC) ); +#endif + +} + +/* + * Call __ASDisplayLayerDecrementConcurrentDisplayCount() upon exit from a display block, matching calls to __ASDisplayLayerIncrementConcurrentDisplayCount(). + */ +static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, BOOL isRasterizing) +{ + // Displays while rasterizing are not counted as concurrent displays, becuase they draw in serial when their rasterizing container displays. + if (isRasterizing) { + return; + } + + if (displayIsAsync) { + dispatch_semaphore_signal(__ASDisplayLayerConcurrentDisplaySemaphore); + } +} + +- (NSObject *)drawParameters +{ + if (_flags.hasDrawParametersForAsyncLayer) { + return [self drawParametersForAsyncLayer:self.asyncLayer]; + } + + 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; + } + + // Capture these outside the display block so they are retained. + UIColor *backgroundColor = self.backgroundColor; + CGRect bounds = self.bounds; + 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); + CGRect 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 and/or a background color. + BOOL shouldDisplay = displayBlock || backgroundColor; + + // 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); + + // 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) { + [image drawInRect:bounds]; + } + } + }; + [displayBlocks addObject:[pushAndDisplayBlock copy]]; + } + + // 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) { + dispatch_block_t popBlock = ^{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextRestoreGState(context); + }; + [displayBlocks addObject:[popBlock copy]]; + } +} + +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock rasterizing:(BOOL)rasterizing +{ + id nodeClass = [self class]; + + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + + ASDisplayNodeAssert(rasterizing || ![self __rasterizedContainerNode], @"Rasterized descendants should never display unless being drawn into the rasterized container."); + + if (!rasterizing && self.shouldRasterizeDescendants) { + CGRect bounds = self.bounds; + if (CGRectIsEmpty(bounds)) { + return nil; + } + + // Collect displayBlocks for all descendants. + NSMutableArray *displayBlocks = [NSMutableArray array]; + [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + + CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay; + BOOL opaque = self.opaque; + + ASDisplayNodeAssert(self.contentsScaleForDisplay != 0.0, @"Invalid contents scale"); + + displayBlock = ^id{ + __ASDisplayLayerIncrementConcurrentDisplayCount(asynchronous, rasterizing); + + if (isCancelledBlock()) { + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return nil; + } + + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + + for (dispatch_block_t block in displayBlocks) { + if (isCancelledBlock()) { + UIGraphicsEndImageContext(); + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return nil; + } + block(); + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + + return image; + }; + } else if (_flags.hasClassDisplay) { + // Capture drawParameters from delegate on main thread + id drawParameters = [self drawParameters]; + + displayBlock = ^id{ + __ASDisplayLayerIncrementConcurrentDisplayCount(asynchronous, rasterizing); + if (isCancelledBlock()) { + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return nil; + } + + UIImage *result = [nodeClass displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return result; + }; + + } else if (_flags.implementsDisplay) { + + CGRect bounds = self.bounds; + if (CGRectIsEmpty(bounds)) { + return nil; + } + + // Capture drawParameters from delegate on main thread + id drawParameters = [self drawParameters]; + CGFloat contentsScaleForDisplay = self.contentsScaleForDisplay; + BOOL opaque = self.opaque; + + displayBlock = ^id{ + __ASDisplayLayerIncrementConcurrentDisplayCount(asynchronous, rasterizing); + + // Short-circuit to be efficient in the case where we've already started a different -display. + if (isCancelledBlock()) { + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return nil; + } + + if (!rasterizing) { + UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + } + + [nodeClass drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + + if (isCancelledBlock()) { + if (!rasterizing) { + UIGraphicsEndImageContext(); + } + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + return nil; + } + + UIImage *image = nil; + if (!rasterizing) { + image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + __ASDisplayLayerDecrementConcurrentDisplayCount(asynchronous, rasterizing); + + return image; + }; + + } + + return [displayBlock copy]; +} + +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + ASDN::MutexLocker l(_propertyLock); + + if ([self __rasterizedContainerNode]) { + return; + } + + // for async display, capture the current displaySentinel value to bail early when the job is executed if another is + // enqueued + // for sync display, just use nil for the displaySentinel and go + // + // REVIEW: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing + // from the displayQueue? do we want to put in some kind of timer to not cancel early fails from displaySentinel + // changes? + ASSentinel *displaySentinel = (asynchronously ? _displaySentinel : nil); + int64_t displaySentinelValue = [displaySentinel increment]; + + asdisplaynode_iscancelled_block_t isCancelledBlock = ^{ + return BOOL(displaySentinelValue != displaySentinel.value); + }; + + // If we're participating in an ancestor's asyncTransaction, find it here + ASDisplayNodeAssert(_layer, @"Expect _layer to be not nil"); + CALayer *containerLayer = _layer.asyncdisplaykit_parentTransactionContainer ?: _layer; + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + + // 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; + } + + // This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously + asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image); + } else { + _layer.contentsScale = image.scale; + _layer.contents = (id)image.CGImage; + } + [self didDisplayAsyncLayer:self.asyncLayer]; + } + }; + + if (displayBlock != NULL) { + // Call willDisplay immediately in either case + if (_flags.hasWillDisplayAsyncLayer) { + [self willDisplayAsyncLayer:self.asyncLayer]; + } + + if (asynchronously) { + [transaction addOperationWithBlock:displayBlock queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + UIImage *contents = (UIImage *)displayBlock(); + completionBlock(contents, NO); + } + } +} + +- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer +{ + [_displaySentinel increment]; +} + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.h b/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.h new file mode 100644 index 0000000000..5e1f6796db --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.h @@ -0,0 +1,20 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@interface ASDisplayNode (DebugTiming) + +@property (nonatomic, readonly) NSTimeInterval debugTimeToCreateView; +@property (nonatomic, readonly) NSTimeInterval debugTimeToApplyPendingState; +@property (nonatomic, readonly) NSTimeInterval debugTimeToAddSubnodeViews; +@property (nonatomic, readonly) NSTimeInterval debugTimeForDidLoad; + +- (NSTimeInterval)debugAllCreationTime; + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.mm b/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.mm new file mode 100644 index 0000000000..dde8b0bd25 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNode+DebugTiming.mm @@ -0,0 +1,88 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDisplayNode+DebugTiming.h" + +#import "ASDisplayNodeInternal.h" + +@implementation ASDisplayNode (DebugTiming) + + +#if TIME_DISPLAYNODE_OPS +- (NSTimeInterval)debugTimeToCreateView +{ + return _debugTimeToCreateView; +} + +- (NSTimeInterval)debugTimeToApplyPendingState +{ + return _debugTimeToApplyPendingState; +} + +- (NSTimeInterval)debugTimeToAddSubnodeViews +{ + return _debugTimeToAddSubnodeViews; +} + +- (NSTimeInterval)debugTimeForDidLoad +{ + return _debugTimeForDidLoad; +} + +- (NSTimeInterval)debugAllCreationTime +{ + return self.debugTimeToCreateView + self.debugTimeToApplyPendingState + self.debugTimeToAddSubnodeViews + self.debugTimeForDidLoad; +} + +// This would over-count views that are created in the parent's didload or addsubnodesubviews, so we need to take a more basic approach +//- (NSTimeInterval)debugRecursiveAllCreationTime +//{ +// __block NSTimeInterval total = 0; +// ASDisplayNodeFindAllSubnodes(self, ^(ASDisplayNode *n){ +// total += self.debugTimeToCreateView; +// total += self.debugTimeToApplyPendingState; +// total += self.debugTimeToAddSubnodeViews; +// total += self.debugTimeForDidLoad; +// return NO; +// }); +// return total; +//} + +#else + +// These ivars are compiled out so we don't have the info available +- (NSTimeInterval)debugTimeToCreateView +{ + return -1; +} + +- (NSTimeInterval)debugTimeToApplyPendingState +{ + return -1; +} + +- (NSTimeInterval)debugTimeToAddSubnodeViews +{ + return -1; +} + +- (NSTimeInterval)debugTimeForDidLoad +{ + return -1; +} + +- (NSTimeInterval)debugAllCreationTime +{ + return -1; +} + +#endif + + + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm new file mode 100644 index 0000000000..b22554e04e --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -0,0 +1,628 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASCoreAnimationExtras.h" +#import "_ASPendingState.h" +#import "ASAssert.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNodeInternal.h" + +/** + * The following macros are conveniences to help in the common tasks related to the bridging that ASDisplayNode does to UIView and CALayer. + * In general, a property can either be: + * - Always sent to the layer or view's layer + * use _getFromLayer / _setToLayer + * - Bridged to the view if view-backed or the layer if layer-backed + * use _getFromViewOrLayer / _setToViewOrLayer / _messageToViewOrLayer + * - Only applicable if view-backed + * use _setToViewOnly / _getFromViewOnly + * - Has differing types on views and layers, or custom ASDisplayNode-specific behavior is desired + * manually implement + * + * _bridge_prologue is defined to either take an appropriate lock or assert thread affinity. Add it at the beginning of any bridged methods. + */ + +#define DISPLAYNODE_USE_LOCKS 1 + +#define __loaded (_layer != nil) + +#if DISPLAYNODE_USE_LOCKS +#define _bridge_prologue ASDisplayNodeAssertThreadAffinity(self); ASDN::MutexLocker l(_propertyLock) +#else +#define _bridge_prologue ASDisplayNodeAssertThreadAffinity(self) +#endif + + +#define _getFromViewOrLayer(layerProperty, viewAndPendingViewStateProperty) __loaded ? \ + (_view ? _view.viewAndPendingViewStateProperty : _layer.layerProperty )\ + : self.pendingViewState.viewAndPendingViewStateProperty + +#define _setToViewOrLayer(layerProperty, layerValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) __loaded ? \ + (_view ? _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) : _layer.layerProperty = (layerValueExpr))\ + : self.pendingViewState.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) + +#define _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) __loaded ? _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) : self.pendingViewState.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) + +#define _getFromViewOnly(viewAndPendingViewStateProperty) __loaded ? _view.viewAndPendingViewStateProperty : self.pendingViewState.viewAndPendingViewStateProperty + +#define _getFromLayer(layerProperty) __loaded ? _layer.layerProperty : self.pendingViewState.layerProperty + +#define _setToLayer(layerProperty, layerValueExpr) __loaded ? _layer.layerProperty = (layerValueExpr) : self.pendingViewState.layerProperty = (layerValueExpr) + +#define _messageToViewOrLayer(viewAndLayerSelector) __loaded ? (_view ? [_view viewAndLayerSelector] : [_layer viewAndLayerSelector]) : [self.pendingViewState viewAndLayerSelector] + +#define _messageToLayer(layerSelector) __loaded ? [_layer layerSelector] : [self.pendingViewState layerSelector] + +/** + * This category implements certainly frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node, + * with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created. + * This allows text sizing in -calculateSizeThatFits: (essentially a simplified layout) to happen off the main thread + * without any CALayer or UIView actually existing while still being able to set and read properties from ASDisplayNode instances. + */ +@implementation ASDisplayNode (UIViewBridge) + +- (CGFloat)alpha +{ + _bridge_prologue; + return _getFromViewOrLayer(opacity, alpha); +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + _bridge_prologue; + _setToViewOrLayer(opacity, newAlpha, alpha, newAlpha); +} + +- (CGFloat)contentsScale +{ + _bridge_prologue; + return _getFromLayer(contentsScale); +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + _bridge_prologue; + _setToLayer(contentsScale, newContentsScale); +} + +- (CGRect)bounds +{ + _bridge_prologue; + return _getFromViewOrLayer(bounds, bounds); +} + +- (void)setBounds:(CGRect)newBounds +{ + _bridge_prologue; + _setToViewOrLayer(bounds, newBounds, bounds, newBounds); +} + +- (CGRect)frame +{ + _bridge_prologue; + + // Frame is only defined when transform is identity. +#if DEBUG + // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. + ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"Must be an identity transform"); +#endif + + CGPoint position = self.position; + CGRect bounds = self.bounds; + CGPoint anchorPoint = self.anchorPoint; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + return CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height); +} + +- (void)setFrame:(CGRect)rect +{ + _bridge_prologue; + + // Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform +#if DEBUG + // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. + ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"Must be an identity transform"); +#endif + + if (_layer && ASDisplayNodeThreadIsMain()) { + CGPoint anchorPoint = _layer.anchorPoint; + _layer.bounds = CGRectMake(0, 0, rect.size.width, rect.size.height); + _layer.position = CGPointMake(rect.origin.x + rect.size.width * anchorPoint.x, + rect.origin.y + rect.size.height * anchorPoint.y); + } else { + CGPoint anchorPoint = self.anchorPoint; + self.bounds = CGRectMake(0, 0, rect.size.width, rect.size.height); + self.position = CGPointMake(rect.origin.x + rect.size.width * anchorPoint.x, + rect.origin.y + rect.size.height * anchorPoint.y); + } +} + +- (void)setNeedsDisplay +{ + ASDisplayNode *rasterizedContainerNode = [self __rasterizedContainerNode]; + if (rasterizedContainerNode) { + [rasterizedContainerNode setNeedsDisplay]; + } else { + [_layer setNeedsDisplay]; + } +} + +- (void)setNeedsLayout +{ + _bridge_prologue; + _messageToViewOrLayer(setNeedsLayout); +} + +- (BOOL)isOpaque +{ + _bridge_prologue; + return _getFromLayer(opaque); +} + +- (void)setOpaque:(BOOL)newOpaque +{ + _bridge_prologue; + _setToLayer(opaque, newOpaque); +} + +- (BOOL)isUserInteractionEnabled +{ + _bridge_prologue; + if (_flags.isLayerBacked) return NO; + return _getFromViewOnly(userInteractionEnabled); +} + +- (void)setUserInteractionEnabled:(BOOL)enabled +{ + _bridge_prologue; + _setToViewOnly(userInteractionEnabled, enabled); +} + +- (BOOL)isExclusiveTouch +{ + _bridge_prologue; + return _getFromViewOnly(exclusiveTouch); +} + +- (void)setExclusiveTouch:(BOOL)exclusiveTouch +{ + _bridge_prologue; + _setToViewOnly(exclusiveTouch, exclusiveTouch); +} + +- (BOOL)clipsToBounds +{ + _bridge_prologue; + return _getFromViewOrLayer(masksToBounds, clipsToBounds); +} + +- (void)setClipsToBounds:(BOOL)clips +{ + _bridge_prologue; + _setToViewOrLayer(masksToBounds, clips, clipsToBounds, clips); +} + +- (CGPoint)anchorPoint +{ + _bridge_prologue; + return _getFromLayer(anchorPoint); +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + _bridge_prologue; + _setToLayer(anchorPoint, newAnchorPoint); +} + +- (CGPoint)position +{ + _bridge_prologue; + return _getFromLayer(position); +} + +- (void)setPosition:(CGPoint)newPosition +{ + _bridge_prologue; + _setToLayer(position, newPosition); +} + +- (CGFloat)zPosition +{ + _bridge_prologue; + return _getFromLayer(zPosition); +} + +- (void)setZPosition:(CGFloat)newPosition +{ + _bridge_prologue; + _setToLayer(zPosition, newPosition); +} + +- (CATransform3D)transform +{ + _bridge_prologue; + return _getFromLayer(transform); +} + +- (void)setTransform:(CATransform3D)newTransform +{ + _bridge_prologue; + _setToLayer(transform, newTransform); +} + +- (CATransform3D)subnodeTransform +{ + _bridge_prologue; + return _getFromLayer(sublayerTransform); +} + +- (void)setSubnodeTransform:(CATransform3D)newSubnodeTransform +{ + _bridge_prologue; + _setToLayer(sublayerTransform, newSubnodeTransform); +} + +- (id)contents +{ + _bridge_prologue; + return _getFromLayer(contents); +} + +- (void)setContents:(id)newContents +{ + _bridge_prologue; + _setToLayer(contents, newContents); +} + +- (BOOL)isHidden +{ + _bridge_prologue; + return _getFromViewOrLayer(hidden, hidden); +} + +- (void)setHidden:(BOOL)flag +{ + _bridge_prologue; + _setToViewOrLayer(hidden, flag, hidden, flag); +} + +- (BOOL)needsDisplayOnBoundsChange +{ + _bridge_prologue; + return _getFromLayer(needsDisplayOnBoundsChange); +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + _bridge_prologue; + _setToLayer(needsDisplayOnBoundsChange, flag); +} + +- (BOOL)autoresizesSubviews +{ + _bridge_prologue; + ASDisplayNodeAssert(!_flags.isLayerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizesSubviews); +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + _bridge_prologue; + ASDisplayNodeAssert(!_flags.isLayerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizesSubviews, flag); +} + +- (UIViewAutoresizing)autoresizingMask +{ + _bridge_prologue; + ASDisplayNodeAssert(!_flags.isLayerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizingMask); +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + _bridge_prologue; + ASDisplayNodeAssert(!_flags.isLayerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizingMask, mask); +} + +- (UIViewContentMode)contentMode +{ + _bridge_prologue; + if (__loaded) { + return ASDisplayNodeUIContentModeFromCAContentsGravity(_layer.contentsGravity); + } else { + return self.pendingViewState.contentMode; + } +} + +- (void)setContentMode:(UIViewContentMode)mode +{ + _bridge_prologue; + if (__loaded) { + _layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(mode); + } else { + self.pendingViewState.contentMode = mode; + } +} + +- (UIColor *)backgroundColor +{ + _bridge_prologue; + return [UIColor colorWithCGColor:_getFromLayer(backgroundColor)]; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + _bridge_prologue; + _setToLayer(backgroundColor, backgroundColor.CGColor); +} + +- (CGColorRef)shadowColor +{ + _bridge_prologue; + return _getFromLayer(shadowColor); +} + +- (void)setShadowColor:(CGColorRef)colorValue +{ + _bridge_prologue; + _setToLayer(shadowColor, colorValue); +} + +- (CGFloat)shadowOpacity +{ + _bridge_prologue; + return _getFromLayer(shadowOpacity); +} + +- (void)setShadowOpacity:(CGFloat)opacity +{ + _bridge_prologue; + _setToLayer(shadowOpacity, opacity); +} + +- (CGSize)shadowOffset +{ + _bridge_prologue; + return _getFromLayer(shadowOffset); +} + +- (void)setShadowOffset:(CGSize)offset +{ + _bridge_prologue; + _setToLayer(shadowOffset, offset); +} + +- (CGFloat)shadowRadius +{ + _bridge_prologue; + return _getFromLayer(shadowRadius); +} + +- (void)setShadowRadius:(CGFloat)radius +{ + _bridge_prologue; + _setToLayer(shadowRadius, radius); +} + +- (CGFloat)borderWidth +{ + _bridge_prologue; + return _getFromLayer(borderWidth); +} + +- (void)setBorderWidth:(CGFloat)width +{ + _bridge_prologue; + _setToLayer(borderWidth, width); +} + +- (CGColorRef)borderColor +{ + _bridge_prologue; + return _getFromLayer(borderColor); +} + +- (void)setBorderColor:(CGColorRef)colorValue +{ + _bridge_prologue; + _setToLayer(borderColor, colorValue); +} + +- (BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue; + return _getFromLayer(allowsEdgeAntialiasing); +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue; + _setToLayer(allowsEdgeAntialiasing, allowsEdgeAntialiasing); +} + +- (unsigned int)edgeAntialiasingMask +{ + _bridge_prologue; + return _getFromLayer(edgeAntialiasingMask); +} + +- (void)setEdgeAntialiasingMask:(unsigned int)edgeAntialiasingMask +{ + _bridge_prologue; + _setToLayer(edgeAntialiasingMask, edgeAntialiasingMask); +} + +- (NSString *)name +{ + _bridge_prologue; + return _getFromLayer(asyncdisplaykit_name); +} + +- (void)setName:(NSString *)name +{ + _bridge_prologue; + _setToLayer(asyncdisplaykit_name, name); +} + +- (BOOL)isAccessibilityElement +{ + _bridge_prologue; + return _getFromViewOnly(isAccessibilityElement); +} + +- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement +{ + _bridge_prologue; + _setToViewOnly(isAccessibilityElement, isAccessibilityElement); +} + +- (NSString *)accessibilityLabel +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityLabel); +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + _bridge_prologue; + _setToViewOnly(accessibilityLabel, accessibilityLabel); +} + +- (NSString *)accessibilityHint +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityHint); +} + +- (void)setAccessibilityHint:(NSString *)accessibilityHint +{ + _bridge_prologue; + _setToViewOnly(accessibilityHint, accessibilityHint); +} + +- (NSString *)accessibilityValue +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityValue); +} + +- (void)setAccessibilityValue:(NSString *)accessibilityValue +{ + _bridge_prologue; + _setToViewOnly(accessibilityValue, accessibilityValue); +} + +- (UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityTraits); +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue; + _setToViewOnly(accessibilityTraits, accessibilityTraits); +} + +- (CGRect)accessibilityFrame +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityFrame); +} + +- (void)setAccessibilityFrame:(CGRect)accessibilityFrame +{ + _bridge_prologue; + _setToViewOnly(accessibilityFrame, accessibilityFrame); +} + +- (NSString *)accessibilityLanguage +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityLanguage); +} + +- (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage +{ + _bridge_prologue; + _setToViewOnly(accessibilityLanguage, accessibilityLanguage); +} + +- (BOOL)accessibilityElementsHidden +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityElementsHidden); +} + +- (void)setAccessibilityElementsHidden:(BOOL)accessibilityElementsHidden +{ + _bridge_prologue; + _setToViewOnly(accessibilityElementsHidden, accessibilityElementsHidden); +} + +- (BOOL)accessibilityViewIsModal +{ + _bridge_prologue; + return _getFromViewOnly(accessibilityViewIsModal); +} + +- (void)setAccessibilityViewIsModal:(BOOL)accessibilityViewIsModal +{ + _bridge_prologue; + _setToViewOnly(accessibilityViewIsModal, accessibilityViewIsModal); +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue; + return _getFromViewOnly(shouldGroupAccessibilityChildren); +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue; + _setToViewOnly(shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren); +} + +@end + + +@implementation ASDisplayNode (ASAsyncTransactionContainer) + +- (BOOL)asyncdisplaykit_isAsyncTransactionContainer +{ + _bridge_prologue; + return _getFromViewOrLayer(asyncdisplaykit_isAsyncTransactionContainer, asyncdisplaykit_isAsyncTransactionContainer); +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer +{ + _bridge_prologue; + _setToViewOrLayer(asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer, asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer); +} + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + ASDisplayNodeAssertMainThread(); + return [_layer asyncdisplaykit_asyncTransactionContainerState]; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + ASDisplayNodeAssertMainThread(); + [_layer asyncdisplaykit_cancelAsyncTransactions]; +} + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ +} + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h new file mode 100644 index 0000000000..4ae501dd67 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -0,0 +1,124 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#import + +#import "_ASDisplayLayer.h" +#import "_AS-objc-internal.h" +#import "ASDisplayNodeExtraIvars.h" +#import "ASSentinel.h" +#import "ASThread.h" + +BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); + +@class _ASPendingState; + +// Allow 2^n increments of begin disabling hierarchy notifications +#define visibilityNotificationsDisabledBits 4 + +#define TIME_DISPLAYNODE_OPS (DEBUG || PROFILE) + +@interface ASDisplayNode () <_ASDisplayLayerDelegate> +{ +@protected + int _retainCount; + ASDN::RecursiveMutex _propertyLock; // Protects access to the _view, _pendingViewState, _subnodes, _supernode, _renderingSubnodes, and other properties which are accessed from multiple threads. + + ASDisplayNode *_supernode; + + ASSentinel *_displaySentinel; + ASSentinel *_replaceAsyncSentinel; + + // This is the desired contentsScale, not the scale at which the layer's contents should be displayed + CGFloat _contentsScaleForDisplay; + + CGSize _size; + CGSize _constrainedSize; + UIEdgeInsets _hitTestSlop; + NSMutableArray *_subnodes; + + Class _viewClass; + Class _layerClass; + UIView *_view; + CALayer *_layer; + + _ASPendingState *_pendingViewState; + + struct { + unsigned implementsDisplay:1; + unsigned isSynchronous:1; + unsigned isLayerBacked:1; + unsigned sizeCalculated:1; + unsigned preventOrCancelDisplay:1; + unsigned displaysAsynchronously:1; + unsigned shouldRasterizeDescendants:1; + unsigned visibilityNotificationsDisabled:visibilityNotificationsDisabledBits; + unsigned isInAppear:1; + unsigned isInDisappear:1; + unsigned inWindow:1; + unsigned hasWillDisplayAsyncLayer:1; + unsigned hasDrawParametersForAsyncLayer:1; + unsigned hasClassDisplay:1; + } _flags; + + ASDisplayNodeExtraIvars _extra; + +#if TIME_DISPLAYNODE_OPS +@public + NSTimeInterval _debugTimeToCreateView; + NSTimeInterval _debugTimeToApplyPendingState; + NSTimeInterval _debugTimeToAddSubnodeViews; + NSTimeInterval _debugTimeForDidLoad; +#endif + +} + +// The _ASDisplayLayer backing the node, if any. +@property (nonatomic, readonly, retain) _ASDisplayLayer *asyncLayer; + +// Creates a pendingViewState if one doesn't exist. Allows setting view properties on a bg thread before there is a view. +@property (atomic, retain, readonly) _ASPendingState *pendingViewState; + +// Swizzle to extend the builtin functionality with custom logic +- (BOOL)__shouldLoadViewOrLayer; + +- (void)__layout; +- (void)__setSupernode:(ASDisplayNode *)supernode; + +// The visibility state of the node. Changed before calling willAppear, willDisappear, and didDisappear. +@property (nonatomic, readwrite, assign, getter = isInWindow) BOOL inWindow; + +// Private API for helper funcitons / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this. +- (BOOL)__visibilityNotificationsDisabled; +- (void)__incrementVisibilityNotificationsDisabled; +- (void)__decrementVisibilityNotificationsDisabled; + +// Call willAppear if necessary and set inWindow = YES if visibility notifications are enabled on all of its parents +- (void)__appear; +// Call willDisappear / didDisappear if necessary and set inWindow = NO if visibility notifications are enabled on all of its parents +- (void)__disappear; + +// Returns the ancestor node that rasterizes descendants, or nil if none. +- (ASDisplayNode *)__rasterizedContainerNode; + +@property (nonatomic, assign) CGFloat contentsScaleForDisplay; + +@end + +@interface UIView (ASDisplayNodeInternal) +@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; +@end + +@interface CALayer (ASDisplayNodeInternal) +@property (nonatomic, assign, readwrite) ASDisplayNode *asyncdisplaykit_node; +@end diff --git a/AsyncDisplayKit/Private/ASImageNode+CGExtras.h b/AsyncDisplayKit/Private/ASImageNode+CGExtras.h new file mode 100644 index 0000000000..e549313c8a --- /dev/null +++ b/AsyncDisplayKit/Private/ASImageNode+CGExtras.h @@ -0,0 +1,32 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#include + +ASDISPLAYNODE_EXTERN_C_BEGIN + + +/** + @abstract Decides how to scale and crop an image to fit in the provided size, while not wasting memory by upscaling images + @param sourceImageSize The size of the encoded image. + @param boundsSize The bounds in which the image will be displayed. + @param contentMode The mode that defines how image will be scaled and cropped to fit. Supported values are UIViewContentModeScaleToAspectFill and UIViewContentModeScaleToAspectFit. + @param cropRect A rectangle that is to be featured by the cropped image. The rectangle is specified as a "unit rectangle," using percentages of the source image's width and height, e.g. CGRectMake(0.5, 0, 0.5, 1.0) will feature the full right half a photo. If the cropRect is empty, the contentMode will be used to determine the drawRect's size, and only the cropRect's origin will be used for positioning. + @discussion If the image is smaller than the size and UIViewContentModeScaleToAspectFill is specified, we suggest the input size so it will be efficiently upscaled on the GPU by the displaying layer at composite time. + */ +extern void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, + CGSize boundsSize, + UIViewContentMode contentMode, + CGRect cropRect, + CGSize *outBackingSize, + CGRect *outDrawRect + ); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Private/ASImageNode+CGExtras.m b/AsyncDisplayKit/Private/ASImageNode+CGExtras.m new file mode 100644 index 0000000000..a3d862700b --- /dev/null +++ b/AsyncDisplayKit/Private/ASImageNode+CGExtras.m @@ -0,0 +1,115 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASImageNode+CGExtras.h" + +// TODO rewrite these to be closer to the intended use -- take UIViewContentMode as param, CGRect destinationBounds, CGSize sourceSize. +static CGSize _ASSizeFillWithAspectRatio(CGFloat aspectRatio, CGSize constraints); +static CGSize _ASSizeFitWithAspectRatio(CGFloat aspectRatio, CGSize constraints); + +static CGSize _ASSizeFillWithAspectRatio(CGFloat sizeToScaleAspectRatio, CGSize destinationSize) +{ + CGFloat destinationAspectRatio = destinationSize.width / destinationSize.height; + if (sizeToScaleAspectRatio > destinationAspectRatio) { + return CGSizeMake(destinationSize.height * sizeToScaleAspectRatio, destinationSize.height); + } else { + return CGSizeMake(destinationSize.width, floorf(destinationSize.width / sizeToScaleAspectRatio)); + } +} + +static CGSize _ASSizeFitWithAspectRatio(CGFloat aspectRatio, CGSize constraints) +{ + CGFloat constraintAspectRatio = constraints.width / constraints.height; + if (aspectRatio > constraintAspectRatio) { + return CGSizeMake(constraints.width, constraints.width / aspectRatio); + } else { + return CGSizeMake(constraints.height * aspectRatio, constraints.height); + } +} + +void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, + CGSize boundsSize, + UIViewContentMode contentMode, + CGRect cropRect, + CGSize *outBackingSize, + CGRect *outDrawRect + ) +{ + + size_t destinationWidth = boundsSize.width; + size_t destinationHeight = boundsSize.height; + + // Often, an image is too low resolution to completely fill the width and height provided. + // Per the API contract as commented in the header, we will adjust input parameters (destinationWidth, destinationHeight) to ensure that the image is not upscaled on the CPU. + CGFloat boundsAspectRatio = (float)destinationWidth / (float)destinationHeight; + + CGSize scaledSizeForImage = sourceImageSize; + BOOL cropToRectDimensions = !CGRectIsEmpty(cropRect); + + if (cropToRectDimensions) { + scaledSizeForImage = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + } else { + if (contentMode == UIViewContentModeScaleAspectFill) + scaledSizeForImage = _ASSizeFillWithAspectRatio(boundsAspectRatio, sourceImageSize); + else if (contentMode == UIViewContentModeScaleAspectFit) + scaledSizeForImage = _ASSizeFitWithAspectRatio(boundsAspectRatio, sourceImageSize); + } + + // If fitting the desired aspect ratio to the image size actually results in a larger buffer, use the input values. + // However, if there is a pixel savings (e.g. we would have to upscale the image), overwrite the function arguments. + if ((scaledSizeForImage.width * scaledSizeForImage.height) < (destinationWidth * destinationHeight)) { + destinationWidth = (size_t)roundf(scaledSizeForImage.width); + destinationHeight = (size_t)roundf(scaledSizeForImage.height); + if (destinationWidth == 0 || destinationHeight == 0) { + *outBackingSize = CGSizeZero; + *outDrawRect = CGRectZero; + return; + } + } + + // Figure out the scaled size within the destination bounds. + CGFloat sourceImageAspectRatio = sourceImageSize.width / sourceImageSize.height; + CGSize scaledSizeForDestination = CGSizeMake(destinationWidth, destinationHeight); + + if (cropToRectDimensions) { + scaledSizeForDestination = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + } else { + if (contentMode == UIViewContentModeScaleAspectFill) + scaledSizeForDestination = _ASSizeFillWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + else if (contentMode == UIViewContentModeScaleAspectFit) + scaledSizeForDestination = _ASSizeFitWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + } + + // Figure out the rectangle into which to draw the image. + CGRect drawRect = CGRectZero; + if (cropToRectDimensions) { + drawRect = CGRectMake(-cropRect.origin.x * scaledSizeForDestination.width, + -cropRect.origin.y * scaledSizeForDestination.height, + scaledSizeForDestination.width, + scaledSizeForDestination.height); + } else { + // We want to obey the origin of cropRect in aspect-fill mode. + if (contentMode == UIViewContentModeScaleAspectFill) { + drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) * cropRect.origin.x), + ((destinationHeight - scaledSizeForDestination.height) * cropRect.origin.y), + scaledSizeForDestination.width, + scaledSizeForDestination.height); + + } + // And otherwise just center it. + else { + drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) / 2.0), + ((destinationHeight - scaledSizeForDestination.height) / 2.0), + scaledSizeForDestination.width, + scaledSizeForDestination.height); + } + } + + *outDrawRect = drawRect; + *outBackingSize = CGSizeMake(destinationWidth, destinationHeight); +} diff --git a/AsyncDisplayKit/Private/ASImageProtocols.h b/AsyncDisplayKit/Private/ASImageProtocols.h new file mode 100644 index 0000000000..1f55c928ca --- /dev/null +++ b/AsyncDisplayKit/Private/ASImageProtocols.h @@ -0,0 +1,56 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +@protocol ASImageCacheProtocol + +@required +/** + @abstract Attempts to fetch an image with the given URL from the cache. + @param URL The URL of the image to retrieve from the cache. + @param callbackQueue The queue to call {@ref completion} on. If this value is nil, @{ref completion} will be invoked on the main-queue. + @param completion The block to be called when the cache has either hit or missed. + @param imageFromCache The image that was retrieved from the cache, if the image could be retrieved; nil otherwise. + @discussion If {@ref URL} is nil, {@ref completion} will be invoked immediately with a nil image. + */ +- (void)fetchCachedImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + completion:(void (^)(CGImageRef imageFromCache))completion; + +@end + +@protocol ASImageDownloaderProtocol + +@required +/** + @abstract Downloads an image with the given URL. + @param URL The URL of the image to download. + @param callbackQueue The queue to call {@ref downloadProgressBlock} and {@ref completion} on. If this value is nil, both blocks will be invoked on the main-queue. + @param downloadProgressBlock The block to be invoked when the download of {@ref URL} progresses. + @param progress The progress of the download, in the range of (0.0, 1.0), inclusive. + @param completion The block to be invoked when the download has completed, or has failed. + @param image The image that was downloaded, if the image could be successfully downloaded; nil otherwise. + @param error An error describing why the download of {@ref URL} failed, if the download failed; nil otherwise. + @discussion If {@ref URL} is nil, {@ref completion} will be invoked immediately with a nil image and an error describing why the download failed. + @result An opaque identifier to be used in canceling the download, via {@ref cancelImageDownloadForIdentifier:}. You must retain the identifier if you wish to use it later. + */ +- (id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgressBlock:(void (^)(CGFloat progress))downloadProgressBlock + completion:(void (^)(CGImageRef image, NSError *error))completion; + +/** + @abstract Cancels an image download. + @param downloadIdentifier The opaque download identifier object returned from {@ref downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:}. + @discussion This method has no effect if {@ref downloadIdentifier} is nil. + */ +- (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier; + +@end diff --git a/AsyncDisplayKit/Private/ASSentinel.h b/AsyncDisplayKit/Private/ASSentinel.h new file mode 100644 index 0000000000..09acb5862b --- /dev/null +++ b/AsyncDisplayKit/Private/ASSentinel.h @@ -0,0 +1,28 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +/** + @summary We want to avoid capturing layer instances on a background queue, but we want a way to cancel rendering + immediately if another display pass begins. ASSentinel is owned by the layer and passed to the background + block. + */ +@interface ASSentinel : NSObject + +/** + Returns the current value of the sentinel. + */ +- (int32_t)value; + +/** + Atomically increments the value and returns the new value. + */ +- (int32_t)increment; + +@end diff --git a/AsyncDisplayKit/Private/ASSentinel.m b/AsyncDisplayKit/Private/ASSentinel.m new file mode 100644 index 0000000000..bf9ee4799e --- /dev/null +++ b/AsyncDisplayKit/Private/ASSentinel.m @@ -0,0 +1,28 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASSentinel.h" + +#import + +@implementation ASSentinel +{ + int32_t _value; +} + +- (int32_t)value +{ + return _value; +} + +- (int32_t)increment +{ + return OSAtomicIncrement32(&_value); +} + +@end diff --git a/AsyncDisplayKit/Private/ASThread.h b/AsyncDisplayKit/Private/ASThread.h new file mode 100644 index 0000000000..8025d3fd50 --- /dev/null +++ b/AsyncDisplayKit/Private/ASThread.h @@ -0,0 +1,326 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#import +#import +#import +#import + +#import +#import + +#import + +static inline BOOL ASDisplayNodeThreadIsMain() +{ + return 0 != pthread_main_np(); +} + +#ifdef __cplusplus + +#define TIME_LOCKER 0 + +#if TIME_LOCKER +#import +#endif + +/** + For use with ASDN::StaticMutex only. + */ +#define ASDISPLAYNODE_MUTEX_INITIALIZER {PTHREAD_MUTEX_INITIALIZER} +#define ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER {PTHREAD_RECURSIVE_MUTEX_INITIALIZER} + +// This MUST always execute, even when assertions are disabled. Otherwise all lock operations become no-ops! +// (To be explicit, do not turn this into an NSAssert, assert(), or any other kind of statement where the +// evaluation of x_ can be compiled out.) +#define ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(x_) do { \ + _Pragma("clang diagnostic push"); \ + _Pragma("clang diagnostic ignored \"-Wunused-variable\""); \ + volatile int res = (x_); \ + assert(res == 0); \ + _Pragma("clang diagnostic pop"); \ +} while (0) + + +namespace ASDN { + + template + class Locker + { + T &_l; + +#if TIME_LOCKER + CFTimeInterval _ti; + const char *_name; +#endif + + public: +#if !TIME_LOCKER + + Locker (T &l) ASDISPLAYNODE_NOTHROW : _l (l) { + _l.lock (); + } + + ~Locker () { + _l.unlock (); + } + + // non-copyable. + Locker(const Locker&) = delete; + Locker &operator=(const Locker&) = delete; + +#else + + Locker (T &l, const char *name = NULL) ASDISPLAYNODE_NOTHROW : _l (l), _name(name) { + _ti = CACurrentMediaTime(); + _l.lock (); + } + + ~Locker () { + _l.unlock (); + if (_name) { + printf(_name, NULL); + printf(" dt:%f\n", CACurrentMediaTime() - _ti); + } + } + +#endif + + }; + + + template + class Unlocker + { + T &_l; + public: + Unlocker (T &l) ASDISPLAYNODE_NOTHROW : _l (l) {_l.unlock ();} + ~Unlocker () {_l.lock ();} + Unlocker(Unlocker&) = delete; + Unlocker &operator=(Unlocker&) = delete; + }; + + struct Mutex + { + /// Constructs a non-recursive mutex (the default). + Mutex () : Mutex (false) {} + + ~Mutex () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_destroy (&_m)); + } + + Mutex (const Mutex&) = delete; + Mutex &operator=(const Mutex&) = delete; + + void lock () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); + } + + void unlock () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_unlock (this->mutex())); + } + + pthread_mutex_t *mutex () { return &_m; } + + protected: + explicit Mutex (bool recursive) { + if (!recursive) { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_init (&_m, NULL)); + } else { + pthread_mutexattr_t attr; + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_init (&attr)); + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE)); + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_init (&_m, &attr)); + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutexattr_destroy (&attr)); + } + } + + private: + pthread_mutex_t _m; + }; + + /** + Obj-C doesn't allow you to pass parameters to C++ ivar constructors. + Provide a convenience to change the default from non-recursive to recursive. + + But wait! Recursive mutexes are a bad idea. Think twice before using one: + + http://www.zaval.org/resources/library/butenhof1.html + http://www.fieryrobot.com/blog/2008/10/14/recursive-locks-will-kill-you/ + */ + struct RecursiveMutex : Mutex + { + RecursiveMutex () : Mutex (true) {} + }; + + typedef Locker MutexLocker; + typedef Unlocker MutexUnlocker; + + /** + If you are creating a static mutex, use StaticMutex and specify its default value as one of ASDISPLAYNODE_MUTEX_INITIALIZER + or ASDISPLAYNODE_MUTEX_RECURSIVE_INITIALIZER. This avoids expensive constructor overhead at startup (or worse, ordering + issues between different static objects). It also avoids running a destructor on app exit time (needless expense). + + Note that you can, but should not, use StaticMutex for non-static objects. It will leak its mutex on destruction, + so avoid that! + + If you fail to specify a default value (like ASDISPLAYNODE_MUTEX_INITIALIZER) an assert will be thrown when you attempt to lock. + */ + struct StaticMutex + { + pthread_mutex_t _m; // public so it can be provided by ASDISPLAYNODE_MUTEX_INITIALIZER and friends + + void lock () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_lock (this->mutex())); + } + + void unlock () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_mutex_unlock (this->mutex())); + } + + pthread_mutex_t *mutex () { return &_m; } + + StaticMutex(const StaticMutex&) = delete; + StaticMutex &operator=(const StaticMutex&) = delete; + }; + + typedef Locker StaticMutexLocker; + typedef Unlocker StaticMutexUnlocker; + + struct SpinLock + { + SpinLock &operator= (bool value) { + _l = value ? ~0 : 0; return *this; + } + + SpinLock() { _l = OS_SPINLOCK_INIT; } + SpinLock(const SpinLock&) = delete; + SpinLock &operator=(const SpinLock&) = delete; + + bool try_lock () { + return OSSpinLockTry (&_l); + } + + void lock () { + OSSpinLockLock(&_l); + } + + void unlock () { + OSSpinLockUnlock(&_l); + } + + OSSpinLock *spinlock () { + return &_l; + } + + private: + OSSpinLock _l; + }; + + typedef Locker SpinLocker; + typedef Unlocker SpinUnlocker; + + struct Condition + { + Condition () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_init(&_c, NULL)); + } + + ~Condition () { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_destroy(&_c)); + } + + // non-copyable. + Condition(const Condition&) = delete; + Condition &operator=(const Condition&) = delete; + + void signal() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_signal(&_c)); + } + + void wait(Mutex &m) { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_cond_wait(&_c, m.mutex())); + } + + pthread_cond_t *condition () { + return &_c; + } + + private: + pthread_cond_t _c; + }; + + struct ReadWriteLock + { + ReadWriteLock() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_init(&_rwlock, NULL)); + } + + ~ReadWriteLock() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_destroy(&_rwlock)); + } + + // non-copyable. + ReadWriteLock(const ReadWriteLock&) = delete; + ReadWriteLock &operator=(const ReadWriteLock&) = delete; + + void readlock() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_rdlock(&_rwlock)); + } + + void writelock() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_wrlock(&_rwlock)); + } + + void unlock() { + ASDISPLAYNODE_THREAD_ASSERT_ON_ERROR(pthread_rwlock_unlock(&_rwlock)); + } + + private: + pthread_rwlock_t _rwlock; + }; + + class ReadWriteLockReadLocker + { + ReadWriteLock &_lock; + public: + ReadWriteLockReadLocker(ReadWriteLock &lock) ASDISPLAYNODE_NOTHROW : _lock(lock) { + _lock.readlock(); + } + + ~ReadWriteLockReadLocker() { + _lock.unlock(); + } + + // non-copyable. + ReadWriteLockReadLocker(const ReadWriteLockReadLocker&) = delete; + ReadWriteLockReadLocker &operator=(const ReadWriteLockReadLocker&) = delete; + }; + + class ReadWriteLockWriteLocker + { + ReadWriteLock &_lock; + public: + ReadWriteLockWriteLocker(ReadWriteLock &lock) ASDISPLAYNODE_NOTHROW : _lock(lock) { + _lock.writelock(); + } + + ~ReadWriteLockWriteLocker() { + _lock.unlock(); + } + + // non-copyable. + ReadWriteLockWriteLocker(const ReadWriteLockWriteLocker&) = delete; + ReadWriteLockWriteLocker &operator=(const ReadWriteLockWriteLocker&) = delete; + }; + +} // namespace ASDN + +#endif /* __cplusplus */ diff --git a/AsyncDisplayKit/Private/_AS-objc-internal.h b/AsyncDisplayKit/Private/_AS-objc-internal.h new file mode 100644 index 0000000000..a8087ab800 --- /dev/null +++ b/AsyncDisplayKit/Private/_AS-objc-internal.h @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2009 Apple Inc. All Rights Reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ + +#ifndef _OBJC_INTERNAL_H +#define _OBJC_INTERNAL_H + +/* + * WARNING DANGER HAZARD BEWARE EEK + * + * Everything in this file is for Apple Internal use only. + * These will change in arbitrary OS updates and in unpredictable ways. + * When your program breaks, you get to keep both pieces. + */ + +/* + * objc-internal.h: Private SPI for use by other system frameworks. + */ + +#include +#include +#include +#include + +__BEGIN_DECLS + +// In-place construction of an Objective-C class. +OBJC_EXPORT Class objc_initializeClassPair(Class superclass_gen, const char *name, Class cls_gen, Class meta_gen) + __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_0); + +#if __OBJC2__ && __LP64__ +// Register a tagged pointer class. +OBJC_EXPORT void _objc_insert_tagged_isa(unsigned char slotNumber, Class isa) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); +#endif + +// Batch object allocation using malloc_zone_batch_malloc(). +OBJC_EXPORT unsigned class_createInstances(Class cls, size_t extraBytes, + id *results, unsigned num_requested) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3) + OBJC_ARC_UNAVAILABLE; + +// Get the isa pointer written into objects just before being freed. +OBJC_EXPORT Class _objc_getFreedObjectClass(void) + __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); + +// Substitute receiver for messages to nil. +// Not supported for all messages to nil. +OBJC_EXPORT id _objc_setNilReceiver(id newNilReceiver) + __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_NA); +OBJC_EXPORT id _objc_getNilReceiver(void) + __OSX_AVAILABLE_STARTING(__MAC_10_3, __IPHONE_NA); + +// Return NO if no instance of `cls` has ever owned an associative reference. +OBJC_EXPORT BOOL class_instancesHaveAssociatedObjects(Class cls) + __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_0); + +// Return YES if GC is on and `object` is a GC allocation. +OBJC_EXPORT BOOL objc_isAuto(id object) + __OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_NA); + +// env NSObjCMessageLoggingEnabled +OBJC_EXPORT void instrumentObjcMessageSends(BOOL flag) + __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0); + +// Initializer called by libSystem +#if __OBJC2__ +OBJC_EXPORT void _objc_init(void) + __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); +#endif + +// GC startup callback from Foundation +OBJC_EXPORT malloc_zone_t *objc_collect_init(int (*callback)(void)) + __OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_NA); + +// Plainly-implemented GC barriers. Rosetta used to use these. +OBJC_EXPORT id objc_assign_strongCast_generic(id value, id *dest) + UNAVAILABLE_ATTRIBUTE; +OBJC_EXPORT id objc_assign_global_generic(id value, id *dest) + UNAVAILABLE_ATTRIBUTE; +OBJC_EXPORT id objc_assign_threadlocal_generic(id value, id *dest) + UNAVAILABLE_ATTRIBUTE; +OBJC_EXPORT id objc_assign_ivar_generic(id value, id dest, ptrdiff_t offset) + UNAVAILABLE_ATTRIBUTE; + +// Install missing-class callback. Used by the late unlamented ZeroLink. +OBJC_EXPORT void _objc_setClassLoader(BOOL (*newClassLoader)(const char *)) OBJC2_UNAVAILABLE; + +// Install handler for allocation failures. +// Handler may abort, or throw, or provide an object to return. +OBJC_EXPORT void _objc_setBadAllocHandler(id (*newHandler)(Class isa)) + __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); + +// This can go away when AppKit stops calling it (rdar://7811851) +#if __OBJC2__ +OBJC_EXPORT void objc_setMultithreaded (BOOL flag) + __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_0,__MAC_10_5, __IPHONE_NA,__IPHONE_NA); +#endif + +// Used by ExceptionHandling.framework +#if !__OBJC2__ +OBJC_EXPORT void _objc_error(id rcv, const char *fmt, va_list args) + __attribute__((noreturn)) + __OSX_AVAILABLE_BUT_DEPRECATED(__MAC_10_0,__MAC_10_5, __IPHONE_NA,__IPHONE_NA); + +#endif + +// External Reference support. Used to support compaction. + +enum { + OBJC_XREF_STRONG = 1, + OBJC_XREF_WEAK = 2 +}; +typedef uintptr_t objc_xref_type_t; +typedef uintptr_t objc_xref_t; + +OBJC_EXPORT objc_xref_t _object_addExternalReference(id object, objc_xref_type_t type) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); +OBJC_EXPORT void _object_removeExternalReference(objc_xref_t xref) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); +OBJC_EXPORT id _object_readExternalReference(objc_xref_t xref) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3); + +OBJC_EXPORT uintptr_t _object_getExternalHash(id object) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// Instance-specific instance variable layout. + +OBJC_EXPORT void _class_setIvarLayoutAccessor(Class cls_gen, const uint8_t* (*accessor) (id object)) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_NA); +OBJC_EXPORT const uint8_t *_object_getIvarLayout(Class cls_gen, id object) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_NA); + +OBJC_EXPORT BOOL _class_usesAutomaticRetainRelease(Class cls) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// Obsolete ARC conversions. + +// hack - remove and reinstate objc.h's definitions +#undef objc_retainedObject +#undef objc_unretainedObject +#undef objc_unretainedPointer +OBJC_EXPORT id objc_retainedObject(objc_objectptr_t pointer) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); +OBJC_EXPORT id objc_unretainedObject(objc_objectptr_t pointer) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); +OBJC_EXPORT objc_objectptr_t objc_unretainedPointer(id object) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); +#if __has_feature(objc_arc) +# define objc_retainedObject(o) ((__bridge_transfer id)(objc_objectptr_t)(o)) +# define objc_unretainedObject(o) ((__bridge id)(objc_objectptr_t)(o)) +# define objc_unretainedPointer(o) ((__bridge objc_objectptr_t)(id)(o)) +#else +# define objc_retainedObject(o) ((id)(objc_objectptr_t)(o)) +# define objc_unretainedObject(o) ((id)(objc_objectptr_t)(o)) +# define objc_unretainedPointer(o) ((objc_objectptr_t)(id)(o)) +#endif + +// API to only be called by root classes like NSObject or NSProxy + +OBJC_EXPORT +id +_objc_rootRetain(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +_objc_rootRelease(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +bool +_objc_rootReleaseWasZero(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +bool +_objc_rootTryRetain(id obj) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +bool +_objc_rootIsDeallocating(id obj) +__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +_objc_rootAutorelease(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +uintptr_t +_objc_rootRetainCount(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +_objc_rootInit(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +_objc_rootAlloc(Class cls) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +_objc_rootDealloc(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +_objc_rootFinalize(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +malloc_zone_t * +_objc_rootZone(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +uintptr_t +_objc_rootHash(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void * +objc_autoreleasePoolPush(void) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +objc_autoreleasePoolPop(void *context) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +OBJC_EXPORT id objc_retain(id obj) + __asm__("_objc_retain") + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT void objc_release(id obj) + __asm__("_objc_release") + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT id objc_autorelease(id obj) + __asm__("_objc_autorelease") + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// wraps objc_autorelease(obj) in a useful way when used with return values +OBJC_EXPORT +id +objc_autoreleaseReturnValue(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// wraps objc_autorelease(objc_retain(obj)) in a useful way when used with return values +OBJC_EXPORT +id +objc_retainAutoreleaseReturnValue(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// called ONLY by ARR by callers to undo the autorelease (if possible), otherwise objc_retain +OBJC_EXPORT +id +objc_retainAutoreleasedReturnValue(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +objc_storeStrong(id *location, id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +objc_retainAutorelease(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// obsolete. +OBJC_EXPORT id objc_retain_autorelease(id obj) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +objc_loadWeakRetained(id *location) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +id +objc_initWeak(id *addr, id val) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +objc_destroyWeak(id *addr) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +objc_copyWeak(id *to, id *from) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +objc_moveWeak(id *to, id *from) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +OBJC_EXPORT +void +_objc_autoreleasePoolPrint(void) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT BOOL objc_should_deallocate(id object) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT void objc_clear_deallocating(id object) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +// to make CF link for now + +OBJC_EXPORT +void * +_objc_autoreleasePoolPush(void) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +OBJC_EXPORT +void +_objc_autoreleasePoolPop(void *context) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + + +// Extra @encode data for XPC, or NULL +OBJC_EXPORT const char *_protocol_getMethodTypeEncoding(Protocol *p, SEL sel, BOOL isRequiredMethod, BOOL isInstanceMethod) + __OSX_AVAILABLE_STARTING(__MAC_10_8, __IPHONE_6_0); + + +// API to only be called by classes that provide their own reference count storage + +OBJC_EXPORT +void +_objc_deallocOnMainThreadHelper(void *context) + __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_5_0); + +// On async versus sync deallocation and the _dealloc2main flag +// +// Theory: +// +// If order matters, then code must always: [self dealloc]. +// If order doesn't matter, then always async should be safe. +// +// Practice: +// +// The _dealloc2main bit is set for GUI objects that may be retained by other +// threads. Once deallocation begins on the main thread, doing more async +// deallocation will at best cause extra UI latency and at worst cause +// use-after-free bugs in unretained delegate style patterns. Yes, this is +// extremely fragile. Yes, in the long run, developers should switch to weak +// references. +// +// Note is NOT safe to do any equality check against the result of +// dispatch_get_current_queue(). The main thread can and does drain more than +// one dispatch queue. That is why we call pthread_main_np(). +// + +typedef enum { + _OBJC_RESURRECT_OBJECT = -1, /* _logicBlock has called -retain, and scheduled a -release for later. */ + _OBJC_DEALLOC_OBJECT_NOW = 1, /* call [self dealloc] immediately. */ + _OBJC_DEALLOC_OBJECT_LATER = 2 /* call [self dealloc] on the main queue. */ +} _objc_object_disposition_t; + +#define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC_BLOCK(_rc_ivar, _logicBlock) \ + -(id)retain { \ + /* this will fail to compile if _rc_ivar is an unsigned type */ \ + int _retain_count_ivar_must_not_be_unsigned[0L - (__typeof__(_rc_ivar))-1] __attribute__((unused)); \ + __typeof__(_rc_ivar) _prev = __sync_fetch_and_add(&_rc_ivar, 2); \ + if (_prev < -2) { /* specifically allow resurrection from logical 0. */ \ + __builtin_trap(); /* BUG: retain of over-released ref */ \ + } \ + return self; \ + } \ + -(oneway void)release { \ + __typeof__(_rc_ivar) _prev = __sync_fetch_and_sub(&_rc_ivar, 2); \ + if (_prev > 0) { \ + return; \ + } else if (_prev < 0) { \ + __builtin_trap(); /* BUG: over-release */ \ + } \ + _objc_object_disposition_t fate = _logicBlock(self); \ + if (fate == _OBJC_RESURRECT_OBJECT) { \ + return; \ + } \ + /* mark the object as deallocating. */ \ + if (!__sync_bool_compare_and_swap(&_rc_ivar, -2, 1)) { \ + __builtin_trap(); /* BUG: dangling ref did a retain */ \ + } \ + if (fate == _OBJC_DEALLOC_OBJECT_NOW) { \ + [self dealloc]; \ + } else if (fate == _OBJC_DEALLOC_OBJECT_LATER) { \ + dispatch_barrier_async_f(dispatch_get_main_queue(), self, \ + _objc_deallocOnMainThreadHelper); \ + } else { \ + __builtin_trap(); /* BUG: bogus fate value */ \ + } \ + } \ + -(NSUInteger)retainCount { \ + return (_rc_ivar + 2) >> 1; \ + } \ + -(BOOL)_tryRetain { \ + __typeof__(_rc_ivar) _prev; \ + do { \ + _prev = _rc_ivar; \ + if (_prev & 1) { \ + return 0; \ + } else if (_prev == -2) { \ + return 0; \ + } else if (_prev < -2) { \ + __builtin_trap(); /* BUG: over-release elsewhere */ \ + } \ + } while ( ! __sync_bool_compare_and_swap(&_rc_ivar, _prev, _prev + 2)); \ + return 1; \ + } \ + -(BOOL)_isDeallocating { \ + if (_rc_ivar == -2) { \ + return 1; \ + } else if (_rc_ivar < -2) { \ + __builtin_trap(); /* BUG: over-release elsewhere */ \ + } \ + return _rc_ivar & 1; \ + } + +#define _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, _dealloc2main) \ + _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC_BLOCK(_rc_ivar, (^(id _self_ __attribute__((unused))) { \ + if (_dealloc2main && !pthread_main_np()) { \ + return _OBJC_DEALLOC_OBJECT_LATER; \ + } else { \ + return _OBJC_DEALLOC_OBJECT_NOW; \ + } \ + })) + +#define _OBJC_SUPPORTED_INLINE_REFCNT(_rc_ivar) _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, 0) +#define _OBJC_SUPPORTED_INLINE_REFCNT_WITH_DEALLOC2MAIN(_rc_ivar) _OBJC_SUPPORTED_INLINE_REFCNT_LOGIC(_rc_ivar, 1) + +__END_DECLS + +#endif diff --git a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h new file mode 100644 index 0000000000..4e41338375 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h @@ -0,0 +1,54 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASBaseDefines.h" + +ASDISPLAYNODE_EXTERN_C_BEGIN + +extern void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image); + +/** + Turns a value of UIViewContentMode to a string for debugging or serialization + @param contentMode Any of the UIViewContentMode constants + @return A human-readable representation of the constant, or the integer value of the constant if not recognized. + */ +extern NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode); + +/** + Turns a string representing a contentMode into a contentMode + @param string Any of the strings in UIContentModeDescriptionLUT + @return Any of the UIViewContentMode constants, or an int if the string is a number. If the string is not recognized, UIViewContentModeScaleToFill is returned. + */ +extern UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string); + +/** + Maps a value of UIViewContentMode to a corresponding contentsGravity + It is worth noting that UIKit and CA have inverse definitions of "top" and "bottom" on iOS, so the corresponding contentsGravity for UIViewContentModeTopLeft is kCAContentsGravityBottomLeft + @param contentMode A content mode except for UIViewContentModeRedraw, which has no corresponding contentsGravity (it corresponds to needsDisplayOnBoundsChange = YES) + @return An NSString constant from the documentation, eg kCAGravityCenter... or nil if there is no corresponding contentsGravity. Will assert if contentMode is unknown. + */ +extern NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode); + +/** + Maps a value of contentsGravity to a corresponding UIViewContentMode + It is worth noting that UIKit and CA have inverse definitions of "top" and "bottom" on iOS, so the corresponding contentMode for kCAContentsGravityBottomLeft is UIViewContentModeTopLeft + @param contentsGravity A contents gravity + @return A UIViewContentMode constant from UIView.h, eg UIViewContentModeCenter..., or UIViewContentModeScaleToFill if contentsGravity is not one of the CA constants. Will assert if the contentsGravity is unknown. + */ +extern UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity); + +/** + Use this to create a stretchable appropriate to approximate a filled rectangle, but with antialiasing on the edges when not pixel-aligned. It's best to keep the layer this image is added to with contentsScale equal to the scale of the final transform to screen space so it is able to antialias appropriately even when you shrink or grow the layer. + @param color the fill color to use in the center of the image + @param innerSize Unfortunately, 4 seems to be the smallest inner size that works if you're applying this stretchable to a larger box, whereas it does not display correctly for larger boxes. Thus some adjustment is necessary for the size of box you're displaying. If you're showing a 1px horizontal line, pass 1 height and at least 4 width. 2px vertical line: 2px wide, 4px high. Passing an innerSize greater that you desire is wasteful + */ +extern UIImage *ASDisplayNodeStretchableBoxContentsWithColor(UIColor *color, CGSize innerSize); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm new file mode 100644 index 0000000000..07532d77cc --- /dev/null +++ b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm @@ -0,0 +1,145 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASCoreAnimationExtras.h" + +#import "ASAssert.h" + +extern void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image) +{ + // FIXME: This method does not currently handle UIImageResizingModeTile, which is the default on iOS 6. + // I'm not sure of a way to use CALayer directly to perform such tiling on the GPU, though the stretch is handled by the GPU, + // and CALayer.h documents the fact that contentsCenter is used to stretch the pixels. + + if (image) { + + // Image may not actually be stretchable in one or both dimensions; this is handled + layer.contents = (id)[image CGImage]; + layer.contentsScale = [image scale]; + layer.rasterizationScale = [image scale]; + CGSize imageSize = [image size]; + + ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero), + @"the resizing mode of image should be stretch; if not, then its insets must be all-zero"); + + UIEdgeInsets insets = [image capInsets]; + + // These are lifted from what UIImageView does by experimentation. Without these exact values, the stretching is slightly off. + const float halfPixelFudge = 0.49f; + const float 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; + } + layer.contentsGravity = kCAGravityResize; + layer.contentsCenter = contentsCenter; + + } else { + layer.contents = nil; + } +} + + +struct _UIContentModeStringLUTEntry { + UIViewContentMode contentMode; + NSString *const string; +}; + +static const struct _UIContentModeStringLUTEntry UIContentModeCAGravityLUT[] = { + {UIViewContentModeScaleToFill, kCAGravityResize}, + {UIViewContentModeScaleAspectFit, kCAGravityResizeAspect}, + {UIViewContentModeScaleAspectFill, kCAGravityResizeAspectFill}, + {UIViewContentModeCenter, kCAGravityCenter}, + {UIViewContentModeTop, kCAGravityBottom}, + {UIViewContentModeBottom, kCAGravityTop}, + {UIViewContentModeLeft, kCAGravityLeft}, + {UIViewContentModeRight, kCAGravityRight}, + {UIViewContentModeTopLeft, kCAGravityBottomLeft}, + {UIViewContentModeTopRight, kCAGravityBottomRight}, + {UIViewContentModeBottomLeft, kCAGravityTopLeft}, + {UIViewContentModeBottomRight, kCAGravityTopRight}, +}; + +static const struct _UIContentModeStringLUTEntry UIContentModeDescriptionLUT[] = { + {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"}, +}; + +NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode) { + for (int i=0; i< ARRAY_COUNT(UIContentModeDescriptionLUT); i++) { + if (UIContentModeDescriptionLUT[i].contentMode == contentMode) { + return UIContentModeDescriptionLUT[i].string; + } + } + return [NSString stringWithFormat:@"%d", (int)contentMode]; +} + +UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string) { + // If you passed one of the constants (this is just an optimization to avoid string comparison) + for (int i=0; i < ARRAY_COUNT(UIContentModeDescriptionLUT); i++) { + if (UIContentModeDescriptionLUT[i].string == string) { + return UIContentModeDescriptionLUT[i].contentMode; + } + } + // If you passed something isEqualToString: to one of the constants + for (int i=0; i < ARRAY_COUNT(UIContentModeDescriptionLUT); i++) { + if ([UIContentModeDescriptionLUT[i].string isEqualToString:string]) { + return UIContentModeDescriptionLUT[i].contentMode; + } + } + return UIViewContentModeScaleToFill; +} + +NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode) +{ + for (int i=0; i < ARRAY_COUNT(UIContentModeCAGravityLUT); i++) { + if (UIContentModeCAGravityLUT[i].contentMode == contentMode) { + return UIContentModeCAGravityLUT[i].string; + } + } + ASDisplayNodeCAssert(contentMode == UIViewContentModeRedraw, @"Encountered an unknown contentMode %d. Is this a new version of iOS?", contentMode); + // Redraw is ok to return nil. + return nil; +} + +UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity) +{ + // If you passed one of the constants (this is just an optimization to avoid string comparison) + for (int i=0; i < ARRAY_COUNT(UIContentModeCAGravityLUT); i++) { + if (UIContentModeCAGravityLUT[i].string == contentsGravity) { + return UIContentModeCAGravityLUT[i].contentMode; + } + } + // If you passed something isEqualToString: to one of the constants + for (int i=0; i < ARRAY_COUNT(UIContentModeCAGravityLUT); i++) { + if ([UIContentModeCAGravityLUT[i].string isEqualToString:contentsGravity]) { + return UIContentModeCAGravityLUT[i].contentMode; + } + } + 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; +} diff --git a/AsyncDisplayKit/Private/_ASPendingState.h b/AsyncDisplayKit/Private/_ASPendingState.h new file mode 100644 index 0000000000..784ab71adf --- /dev/null +++ b/AsyncDisplayKit/Private/_ASPendingState.h @@ -0,0 +1,30 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "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 + +// Supports all of the properties included the ASDisplayNodeView protocol + +- (void)applyToView:(UIView *)view; +- (void)applyToLayer:(CALayer *)layer; + +@end diff --git a/AsyncDisplayKit/Private/_ASPendingState.m b/AsyncDisplayKit/Private/_ASPendingState.m new file mode 100644 index 0000000000..1535e51ce0 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASPendingState.m @@ -0,0 +1,777 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "_ASPendingState.h" + +#import "_ASCoreAnimationExtras.h" +#import "_ASAsyncTransactionContainer.h" +#import "ASAssert.h" + +@implementation _ASPendingState +{ + @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency + + UIViewAutoresizing autoresizingMask; + unsigned int edgeAntialiasingMask; + CGRect bounds; + CGColorRef backgroundColor; + id contents; + CGFloat alpha; + UIViewContentMode contentMode; + CGPoint anchorPoint; + CGPoint position; + CGFloat zPosition; + CGFloat contentsScale; + CATransform3D transform; + CATransform3D sublayerTransform; + CGColorRef shadowColor; + CGFloat shadowOpacity; + CGSize shadowOffset; + CGFloat shadowRadius; + CGFloat borderWidth; + CGColorRef borderColor; + BOOL asyncTransactionContainer; + NSString *name; + BOOL isAccessibilityElement; + NSString *accessibilityLabel; + NSString *accessibilityHint; + NSString *accessibilityValue; + UIAccessibilityTraits accessibilityTraits; + CGRect accessibilityFrame; + NSString *accessibilityLanguage; + BOOL accessibilityElementsHidden; + BOOL accessibilityViewIsModal; + BOOL shouldGroupAccessibilityChildren; + + struct { + // Properties + int needsDisplay:1; + int needsLayout:1; + + // Flags indicating that a given property should be applied to the view at creation + int setClipsToBounds:1; + int setOpaque:1; + int setNeedsDisplayOnBoundsChange:1; + int setAutoresizesSubviews:1; + int setAutoresizingMask:1; + int setBounds:1; + int setBackgroundColor:1; + int setContents:1; + int setHidden:1; + int setAlpha:1; + int setContentMode:1; + int setNeedsDisplay:1; + int setAnchorPoint:1; + int setPosition:1; + int setZPosition:1; + int setContentsScale:1; + int setTransform:1; + int setSublayerTransform:1; + int setUserInteractionEnabled:1; + int setExclusiveTouch:1; + int setShadowColor:1; + int setShadowOpacity:1; + int setShadowOffset:1; + int setShadowRadius:1; + int setBorderWidth:1; + int setBorderColor:1; + int setAsyncTransactionContainer:1; + int setName:1; + int setAllowsEdgeAntialiasing:1; + int setEdgeAntialiasingMask:1; + int setIsAccessibilityElement:1; + int setAccessibilityLabel:1; + int setAccessibilityHint:1; + int setAccessibilityValue:1; + int setAccessibilityTraits:1; + int setAccessibilityFrame:1; + int setAccessibilityLanguage:1; + int setAccessibilityElementsHidden:1; + int setAccessibilityViewIsModal:1; + int setShouldGroupAccessibilityChildren:1; + } _flags; +} + + +@synthesize clipsToBounds=clipsToBounds; +@synthesize opaque=opaque; +@synthesize bounds=bounds; +@synthesize backgroundColor=backgroundColor; +@synthesize contents=contents; +@synthesize hidden=isHidden; +@synthesize needsDisplayOnBoundsChange=needsDisplayOnBoundsChange; +@synthesize allowsEdgeAntialiasing=allowsEdgeAntialiasing; +@synthesize edgeAntialiasingMask=edgeAntialiasingMask; +@synthesize autoresizesSubviews=autoresizesSubviews; +@synthesize autoresizingMask=autoresizingMask; +@synthesize alpha=alpha; +@synthesize contentMode=contentMode; +@synthesize anchorPoint=anchorPoint; +@synthesize position=position; +@synthesize zPosition=zPosition; +@synthesize contentsScale=contentsScale; +@synthesize transform=transform; +@synthesize sublayerTransform=sublayerTransform; +@synthesize userInteractionEnabled=userInteractionEnabled; +@synthesize exclusiveTouch=exclusiveTouch; +@synthesize shadowColor=shadowColor; +@synthesize shadowOpacity=shadowOpacity; +@synthesize shadowOffset=shadowOffset; +@synthesize shadowRadius=shadowRadius; +@synthesize borderWidth=borderWidth; +@synthesize borderColor=borderColor; +@synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; +@synthesize asyncdisplaykit_name=name; + +- (id)init +{ + if (!(self = [super init])) + return nil; + + // Default UIKit color is an RGB color + static CGColorRef black; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + black = CGColorCreate(colorSpace, (CGFloat[]){0,0,0,1} ); + CFRetain(black); + CGColorSpaceRelease(colorSpace); + }); + + // Set defaults, these come from the defaults specified in CALayer and UIView + clipsToBounds = NO; + opaque = YES; + bounds = CGRectZero; + backgroundColor = nil; + contents = nil; + isHidden = NO; + needsDisplayOnBoundsChange = NO; + autoresizesSubviews = YES; + alpha = 1.0f; + contentMode = UIViewContentModeScaleToFill; + _flags.needsDisplay = NO; + anchorPoint = CGPointMake(0.5, 0.5); + position = CGPointZero; + zPosition = 0.0; + contentsScale = 1.0f; + transform = CATransform3DIdentity; + sublayerTransform = CATransform3DIdentity; + userInteractionEnabled = YES; + CFRetain(black); + shadowColor = black; + shadowOpacity = 0.0; + shadowOffset = CGSizeMake(0, -3); + shadowRadius = 3; + borderWidth = 0; + CFRetain(black); + borderColor = black; + isAccessibilityElement = NO; + accessibilityLabel = nil; + accessibilityHint = nil; + accessibilityValue = nil; + accessibilityTraits = UIAccessibilityTraitNone; + accessibilityFrame = CGRectZero; + accessibilityLanguage = nil; + accessibilityElementsHidden = NO; + accessibilityViewIsModal = NO; + shouldGroupAccessibilityChildren = NO; + edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerRightEdge | kCALayerTopEdge | kCALayerBottomEdge); + + return self; +} + +- (void)dealloc +{ + [contents release]; + [name release]; + if (NULL != shadowColor) + CFRelease(shadowColor); + if (NULL != borderColor) + CFRelease(borderColor); + if (NULL != backgroundColor) + CFRelease(backgroundColor); + [accessibilityLabel release]; + [accessibilityHint release]; + [accessibilityValue release]; + [accessibilityLanguage release]; + [super dealloc]; +} + +- (CALayer *)layer +{ + ASDisplayNodeAssert(NO, @"One shouldn't call node.layer when the view isn't loaded, but we're returning nil to not crash if someone is still doing this"); + return nil; +} + +- (void)setNeedsDisplay +{ + _flags.needsDisplay = YES; +} + +- (void)setNeedsLayout +{ + _flags.needsLayout = YES; +} + +- (void)setClipsToBounds:(BOOL)flag +{ + clipsToBounds = flag; + _flags.setClipsToBounds = YES; +} + +- (void)setOpaque:(BOOL)flag +{ + opaque = flag; + _flags.setOpaque = YES; +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + needsDisplayOnBoundsChange = flag; + _flags.setNeedsDisplayOnBoundsChange = YES; +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)flag +{ + allowsEdgeAntialiasing = flag; + _flags.setAllowsEdgeAntialiasing = YES; +} + +- (void)setEdgeAntialiasingMask:(unsigned int)mask +{ + edgeAntialiasingMask = mask; + _flags.setEdgeAntialiasingMask = YES; +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + autoresizesSubviews = flag; + _flags.setAutoresizesSubviews = YES; +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + autoresizingMask = mask; + _flags.setAutoresizingMask = YES; +} + +- (void)setBounds:(CGRect)newBounds +{ + bounds = newBounds; + _flags.setBounds = YES; +} + +- (CGColorRef)backgroundColor +{ + return backgroundColor; +} + +- (void)setBackgroundColor:(CGColorRef)color +{ + if (color == backgroundColor) { + return; + } + + CGColorRelease(backgroundColor); + backgroundColor = CGColorRetain(color); + _flags.setBackgroundColor = YES; +} + +- (void)setContents:(id)newContents +{ + if (contents == newContents) { + return; + } + + [contents release]; + contents = [newContents retain]; + _flags.setContents = YES; +} + +- (void)setHidden:(BOOL)flag +{ + isHidden = flag; + _flags.setHidden = YES; +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + alpha = newAlpha; + _flags.setAlpha = YES; +} + +- (void)setContentMode:(UIViewContentMode)newContentMode +{ + contentMode = newContentMode; + _flags.setContentMode = YES; +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + anchorPoint = newAnchorPoint; + _flags.setAnchorPoint = YES; +} + +- (void)setPosition:(CGPoint)newPosition +{ + position = newPosition; + _flags.setPosition = YES; +} + +- (void)setZPosition:(CGFloat)newPosition +{ + zPosition = newPosition; + _flags.setZPosition = YES; +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + contentsScale = newContentsScale; + _flags.setContentsScale = YES; +} + +- (void)setTransform:(CATransform3D)newTransform +{ + transform = newTransform; + _flags.setTransform = YES; +} + +- (void)setSublayerTransform:(CATransform3D)newSublayerTransform +{ + sublayerTransform = newSublayerTransform; + _flags.setSublayerTransform = YES; +} + +- (void)setUserInteractionEnabled:(BOOL)flag +{ + userInteractionEnabled = flag; + _flags.setUserInteractionEnabled = YES; +} + +- (void)setExclusiveTouch:(BOOL)flag +{ + exclusiveTouch = flag; + _flags.setExclusiveTouch = YES; +} + +- (void)setShadowColor:(CGColorRef)color +{ + if (shadowColor == color) { + return; + } + + CGColorRelease(shadowColor); + shadowColor = color; + CGColorRetain(shadowColor); + + _flags.setShadowColor = YES; +} + +- (void)setShadowOpacity:(CGFloat)newOpacity +{ + shadowOpacity = newOpacity; + _flags.setShadowOpacity = YES; +} + +- (void)setShadowOffset:(CGSize)newOffset +{ + shadowOffset = newOffset; + _flags.setShadowOffset = YES; +} + +- (void)setShadowRadius:(CGFloat)newRadius +{ + shadowRadius = newRadius; + _flags.setShadowRadius = YES; +} + +- (void)setBorderWidth:(CGFloat)newWidth +{ + borderWidth = newWidth; + _flags.setBorderWidth = YES; +} + +- (void)setBorderColor:(CGColorRef)color +{ + if (borderColor == color) { + return; + } + + CGColorRelease(borderColor); + borderColor = color; + CGColorRetain(borderColor); + + _flags.setBorderColor = YES; +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)flag +{ + asyncTransactionContainer = flag; + _flags.setAsyncTransactionContainer = YES; +} + +// This is named this way, since I'm not sure we can change the setter for the CA version +- (void)setAsyncdisplaykit_name:(NSString *)newName +{ + _flags.setName = YES; + if (name != newName) { + [name release]; + name = [newName copy]; + } +} + +- (NSString *)asyncdisplaykit_name +{ + return [[name retain] autorelease]; +} + +- (BOOL)isAccessibilityElement +{ + return isAccessibilityElement; +} + +- (void)setIsAccessibilityElement:(BOOL)newIsAccessibilityElement +{ + isAccessibilityElement = newIsAccessibilityElement; + _flags.setIsAccessibilityElement = YES; +} + +- (NSString *)accessibilityLabel +{ + return [[accessibilityLabel retain] autorelease]; +} + +- (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel +{ + _flags.setAccessibilityLabel = YES; + if (accessibilityLabel != newAccessibilityLabel) { + [accessibilityLabel release]; + accessibilityLabel = [newAccessibilityLabel copy]; + } +} + +- (NSString *)accessibilityHint +{ + return [[accessibilityHint retain] autorelease]; +} + +- (void)setAccessibilityHint:(NSString *)newAccessibilityHint +{ + _flags.setAccessibilityHint = YES; + [accessibilityHint release]; + accessibilityHint = [newAccessibilityHint copy]; +} + +- (NSString *)accessibilityValue +{ + return [[accessibilityValue retain] autorelease]; +} + +- (void)setAccessibilityValue:(NSString *)newAccessibilityValue +{ + _flags.setAccessibilityValue = YES; + [accessibilityValue release]; + accessibilityValue = [newAccessibilityValue copy]; +} + +- (UIAccessibilityTraits)accessibilityTraits +{ + return accessibilityTraits; +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)newAccessibilityTraits +{ + accessibilityTraits = newAccessibilityTraits; + _flags.setAccessibilityTraits = YES; +} + +- (CGRect)accessibilityFrame +{ + return accessibilityFrame; +} + +- (void)setAccessibilityFrame:(CGRect)newAccessibilityFrame +{ + accessibilityFrame = newAccessibilityFrame; + _flags.setAccessibilityFrame = YES; +} + +- (NSString *)accessibilityLanguage +{ + return [[accessibilityLanguage retain] autorelease]; +} + +- (void)setAccessibilityLanguage:(NSString *)newAccessibilityLanguage +{ + _flags.setAccessibilityLanguage = YES; + if(accessibilityLanguage != newAccessibilityLanguage) { + [accessibilityLanguage release]; + accessibilityLanguage = [newAccessibilityLanguage retain]; + } +} + +- (BOOL)accessibilityElementsHidden +{ + return accessibilityElementsHidden; +} + +- (void)setAccessibilityElementsHidden:(BOOL)newAccessibilityElementsHidden +{ + accessibilityElementsHidden = newAccessibilityElementsHidden; + _flags.setAccessibilityElementsHidden = YES; +} + +- (BOOL)accessibilityViewIsModal +{ + return accessibilityViewIsModal; +} + +- (void)setAccessibilityViewIsModal:(BOOL)newAccessibilityViewIsModal +{ + accessibilityViewIsModal = newAccessibilityViewIsModal; + _flags.setAccessibilityViewIsModal = YES; +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + return shouldGroupAccessibilityChildren; +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)newShouldGroupAccessibilityChildren +{ + shouldGroupAccessibilityChildren = newShouldGroupAccessibilityChildren; + _flags.setShouldGroupAccessibilityChildren = YES; +} + +- (void)applyToLayer:(CALayer *)layer +{ + if (_flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (_flags.setPosition) + layer.position = position; + + if (_flags.setZPosition) + layer.zPosition = zPosition; + + if (_flags.setBounds) + layer.bounds = bounds; + + if (_flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (_flags.setTransform) + layer.transform = transform; + + if (_flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (_flags.setContents) + layer.contents = contents; + + if (_flags.setClipsToBounds) + layer.masksToBounds = clipsToBounds; + + if (_flags.setBackgroundColor) + layer.backgroundColor = backgroundColor; + + if (_flags.setOpaque) + layer.opaque = opaque; + + if (_flags.setHidden) + layer.hidden = isHidden; + + if (_flags.setAlpha) + layer.opacity = alpha; + + if (_flags.setContentMode) + layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); + + if (_flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (_flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (_flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (_flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (_flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (_flags.setBorderColor) + layer.borderColor = borderColor; + + if (_flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (_flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (_flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (_flags.needsDisplay) + [layer setNeedsDisplay]; + + if (_flags.needsLayout) + [layer setNeedsLayout]; + + if (_flags.setAsyncTransactionContainer) + layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (_flags.setName) + layer.asyncdisplaykit_name = name; + + if (_flags.setOpaque) + ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); +} + +- (void)applyToView:(UIView *)view +{ + /* + Use our convenience setters blah here instead of layer.blah + We were accidentally setting some properties on layer here, but view in UIViewBridgeOptimizations. + + That could easily cause bugs where it mattered whether you set something up on a bg thread on in -didLoad + because a different setter would be called. + */ + + CALayer *layer = view.layer; + + if (_flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (_flags.setPosition) + layer.position = position; + + if (_flags.setZPosition) + layer.zPosition = zPosition; + + if (_flags.setBounds) + view.bounds = bounds; + + if (_flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (_flags.setTransform) + layer.transform = transform; + + if (_flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (_flags.setContents) + layer.contents = contents; + + if (_flags.setClipsToBounds) + view.clipsToBounds = clipsToBounds; + + if (_flags.setBackgroundColor) + layer.backgroundColor = backgroundColor; + + if (_flags.setOpaque) + view.layer.opaque = opaque; + + if (_flags.setHidden) + view.hidden = isHidden; + + if (_flags.setAlpha) + view.alpha = alpha; + + if (_flags.setContentMode) + view.contentMode = contentMode; + + if (_flags.setUserInteractionEnabled) + view.userInteractionEnabled = userInteractionEnabled; + + if (_flags.setExclusiveTouch) + view.exclusiveTouch = exclusiveTouch; + + if (_flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (_flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (_flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (_flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (_flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (_flags.setBorderColor) + layer.borderColor = borderColor; + + if (_flags.setAutoresizingMask) + view.autoresizingMask = autoresizingMask; + + if (_flags.setAutoresizesSubviews) + view.autoresizesSubviews = autoresizesSubviews; + + if (_flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (_flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (_flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (_flags.needsDisplay) + [view setNeedsDisplay]; + + if (_flags.needsLayout) + [view setNeedsLayout]; + + if (_flags.setAsyncTransactionContainer) + view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (_flags.setName) + layer.asyncdisplaykit_name = name; + + if (_flags.setOpaque) + ASDisplayNodeAssert(view.layer.opaque == opaque, @"Didn't set opaque as desired"); + + if (_flags.setIsAccessibilityElement) + view.isAccessibilityElement = isAccessibilityElement; + + if (_flags.setAccessibilityLabel) + view.accessibilityLabel = accessibilityLabel; + + if (_flags.setAccessibilityHint) + view.accessibilityHint = accessibilityHint; + + if (_flags.setAccessibilityValue) + view.accessibilityValue = accessibilityValue; + + if (_flags.setAccessibilityTraits) + view.accessibilityTraits = accessibilityTraits; + + if (_flags.setAccessibilityFrame) + view.accessibilityFrame = accessibilityFrame; + + if (_flags.setAccessibilityLanguage) + view.accessibilityLanguage = accessibilityLanguage; + + if (_flags.setAccessibilityElementsHidden) + view.accessibilityElementsHidden = accessibilityElementsHidden; + + if (_flags.setAccessibilityViewIsModal) + view.accessibilityViewIsModal = accessibilityViewIsModal; + + if (_flags.setShouldGroupAccessibilityChildren) + view.shouldGroupAccessibilityChildren = shouldGroupAccessibilityChildren; +} + +@end diff --git a/AsyncDisplayKit/Private/_ASScopeTimer.h b/AsyncDisplayKit/Private/_ASScopeTimer.h new file mode 100644 index 0000000000..0412bc8399 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASScopeTimer.h @@ -0,0 +1,38 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +/** + Must compile as c++ for this to work. + + Usage: + // Can be an ivar or local variable + NSTimeInterval placeToStoreTiming; + + { + // some scope + ASDisplayNode::ScopeTimer t(placeToStoreTiming); + DoPotentiallySlowWork(); + MorePotentiallySlowWork(); + } + + */ + +namespace ASDN { + struct ScopeTimer { + NSTimeInterval begin; + NSTimeInterval &outT; + ScopeTimer(NSTimeInterval &outRef) : outT(outRef) { + begin = CACurrentMediaTime(); + } + ~ScopeTimer() { + outT = CACurrentMediaTime() - begin; + } + }; +} diff --git a/AsyncDisplayKitTests/ASDisplayLayerTests.m b/AsyncDisplayKitTests/ASDisplayLayerTests.m new file mode 100644 index 0000000000..723b7867e1 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayLayerTests.m @@ -0,0 +1,609 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +#import + +#import "_ASDisplayLayer.h" +#import "_ASAsyncTransactionContainer.h" +#import "ASDisplayNode.h" +#import "ASDisplayNodeTestsHelper.h" + +static UIImage *bogusImage() { + static UIImage *bogusImage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + UIGraphicsBeginImageContext(CGSizeMake(10, 10)); + + bogusImage = [UIGraphicsGetImageFromCurrentImageContext() retain]; + + UIGraphicsEndImageContext(); + + }); + + return bogusImage; +} + +@interface _ASDisplayLayerTestContainerLayer : CALayer +@property (nonatomic, assign, readonly) NSUInteger didCompleteTransactionCount; +@end + +@implementation _ASDisplayLayerTestContainerLayer + +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction +{ + _didCompleteTransactionCount++; +} + +@end + + +@interface _ASDisplayLayerTestLayer : _ASDisplayLayer +{ + BOOL _isInCancelAsyncDisplay; + BOOL _isInDisplay; +} +@property (nonatomic, assign, readonly) NSUInteger displayCount; +@property (nonatomic, assign, readonly) NSUInteger drawInContextCount; +@property (nonatomic, assign, readonly) NSUInteger setContentsAsyncCount; +@property (nonatomic, assign, readonly) NSUInteger setContentsSyncCount; +@property (nonatomic, copy, readonly) NSString *setContentsCounts; +- (BOOL)checkSetContentsCountsWithSyncCount:(NSUInteger)syncCount asyncCount:(NSUInteger)asyncCount; +@end + +@implementation _ASDisplayLayerTestLayer + +- (NSString *)setContentsCounts +{ + return [NSString stringWithFormat:@"syncCount:%u, asyncCount:%u", _setContentsSyncCount, _setContentsAsyncCount]; +} + +- (BOOL)checkSetContentsCountsWithSyncCount:(NSUInteger)syncCount asyncCount:(NSUInteger)asyncCount +{ + return ((syncCount == _setContentsSyncCount) && + (asyncCount == _setContentsAsyncCount)); +} + +- (void)setContents:(id)contents +{ + [super setContents:contents]; + + if (self.displaysAsynchronously) { + if (_isInDisplay) { + [[NSException exceptionWithName:NSInvalidArgumentException + reason:@"There is no placeholder logic in _ASDisplayLayer, unknown caller for setContents:" + userInfo:nil] raise]; + } else if (!_isInCancelAsyncDisplay) { + _setContentsAsyncCount++; + } + } else { + _setContentsSyncCount++; + } +} + +- (void)display +{ + _isInDisplay = YES; + [super display]; + _isInDisplay = NO; + _displayCount++; +} + +- (void)cancelAsyncDisplay +{ + _isInCancelAsyncDisplay = YES; + [super cancelAsyncDisplay]; + _isInCancelAsyncDisplay = NO; +} + +// This should never get called. This just records if it is. +- (void)drawInContext:(CGContextRef)context +{ + [super drawInContext:context]; + _drawInContextCount++; +} + +@end + +typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateMode) +{ + _ASDisplayLayerTestDelegateModeNone = 0, + _ASDisplayLayerTestDelegateModeDrawParameters = 1 << 0, + _ASDisplayLayerTestDelegateModeWillDisplay = 1 << 1, + _ASDisplayLayerTestDelegateModeDidDisplay = 1 << 2, +}; + +typedef NS_ENUM(NSUInteger, _ASDisplayLayerTestDelegateClassModes) { + _ASDisplayLayerTestDelegateClassModeNone = 0, + _ASDisplayLayerTestDelegateClassModeDisplay = 1 << 0, + _ASDisplayLayerTestDelegateClassModeDrawInContext = 1 << 1, +}; + +@interface _ASDisplayLayerTestDelegate : ASDisplayNode <_ASDisplayLayerDelegate> + +@property (nonatomic, assign) NSUInteger didDisplayCount; +@property (nonatomic, assign) NSUInteger drawParametersCount; +@property (nonatomic, assign) NSUInteger willDisplayCount; + +// for _ASDisplayLayerTestDelegateModeClassDisplay +@property (nonatomic, assign) NSUInteger displayCount; +@property (nonatomic, copy) UIImage *(^displayLayerBlock)(); + +// for _ASDisplayLayerTestDelegateModeClassDrawInContext +@property (nonatomic, assign) NSUInteger drawRectCount; + +@end + +@implementation _ASDisplayLayerTestDelegate { + _ASDisplayLayerTestDelegateMode _modes; +} + +static _ASDisplayLayerTestDelegateClassModes _class_modes; + ++ (void)setClassModes:(_ASDisplayLayerTestDelegateClassModes)classModes +{ + _class_modes = classModes; +} + +- (id)initWithModes:(_ASDisplayLayerTestDelegateMode)modes +{ + _modes = modes; + + if (!(self = [super initWithLayerClass:[_ASDisplayLayerTestLayer class]])) + return nil; + + return self; +} + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + _didDisplayCount++; +} + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + _drawParametersCount++; + return self; +} + +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + _willDisplayCount++; +} + +- (BOOL)respondsToSelector:(SEL)selector +{ + if (sel_isEqual(selector, @selector(didDisplayAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeDidDisplay); + } else if (sel_isEqual(selector, @selector(drawParametersForAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeDrawParameters); + } else if (sel_isEqual(selector, @selector(willDisplayAsyncLayer:))) { + return (_modes & _ASDisplayLayerTestDelegateModeWillDisplay); + } else { + return [super respondsToSelector:selector]; + } +} + ++ (BOOL)respondsToSelector:(SEL)selector +{ + if (sel_isEqual(selector, @selector(displayWithParameters:isCancelled:))) { + return _class_modes & _ASDisplayLayerTestDelegateClassModeDisplay; + } else if (sel_isEqual(selector, @selector(drawRect:withParameters:isCancelled:isRasterizing:))) { + return _class_modes & _ASDisplayLayerTestDelegateClassModeDrawInContext; + } else { + return [super respondsToSelector:selector]; + } +} + +// DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! ++ (UIImage *)displayWithParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock +{ + UIImage *contents = bogusImage(); + if (delegate->_displayLayerBlock != NULL) { + contents = delegate->_displayLayerBlock(); + } + delegate->_displayCount++; + return contents; +} + +// DANGER: Don't use the delegate as the parameters in real code; this is not thread-safe and just for accounting in unit tests! ++ (void)drawRect:(CGRect)bounds withParameters:(_ASDisplayLayerTestDelegate *)delegate isCancelled:(asdisplaynode_iscancelled_block_t)sentinelBlock isRasterizing:(BOOL)isRasterizing +{ + delegate->_drawRectCount++; +} + +- (void)dealloc +{ + [_displayLayerBlock release]; + [super dealloc]; +} + +@end + +@interface _ASDisplayLayerTests : XCTestCase +@end + +@implementation _ASDisplayLayerTests + +- (void)setUp { + [super setUp]; + // Force bogusImage() to create+cache its image. This impacts any time-sensitive tests which call the method from + // within the timed portion of the test. It seems that, in rare cases, this image creation can take a bit too long, + // causing a test failure. + bogusImage(); +} + +// since we're not running in an application, we need to force this display on layer the hierarchy +- (void)displayLayerRecursively:(CALayer *)layer +{ + if (layer.needsDisplay) { + [layer displayIfNeeded]; + } + for (CALayer *sublayer in layer.sublayers) { + [self displayLayerRecursively:sublayer]; + } +} + +- (void)waitForDisplayQueue +{ + // make sure we don't lock up the tests indefinitely; fail after 1 sec by using an async barrier + __block BOOL didHitBarrier = NO; + dispatch_barrier_async([_ASDisplayLayer displayQueue], ^{ + didHitBarrier = YES; + }); + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return didHitBarrier; })); +} + +- (void)waitForLayer:(_ASDisplayLayerTestLayer *)layer asyncDisplayCount:(NSUInteger)count +{ + // make sure we don't lock up the tests indefinitely; fail after 1 sec of waiting for the setContents async count to increment + // NOTE: the layer sets its contents async back on the main queue, so we need to wait for main + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (layer.setContentsAsyncCount == count); + })); +} + +- (void)waitForAsyncDelegate:(_ASDisplayLayerTestDelegate *)asyncDelegate +{ + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (asyncDelegate.didDisplayCount == 1); + })); +} + +- (void)checkDelegateDisplay:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 1u); + + [asyncDelegate release]; +} + +- (void)testDelegateDisplaySync +{ + [self checkDelegateDisplay:NO]; +} + +- (void)testDelegateDisplayAsync +{ + [self checkDelegateDisplay:YES]; +} + +- (void)checkDelegateDrawInContext:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDrawInContext]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForLayer:layer asyncDisplayCount:1]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 1u); + + [asyncDelegate release]; +} + +- (void)testDelegateDrawInContextSync +{ + [self checkDelegateDrawInContext:NO]; +} + +- (void)testDelegateDrawInContextAsync +{ + [self checkDelegateDrawInContext:YES]; +} + +- (void)checkDelegateDisplayAndDrawInContext:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay | _ASDisplayLayerTestDelegateClassModeDrawInContext]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + [self waitForAsyncDelegate:asyncDelegate]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 1u); + XCTAssertEqual(asyncDelegate.displayCount, 1u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + + [asyncDelegate release]; +} + +- (void)testDelegateDisplayAndDrawInContextSync +{ + [self checkDelegateDisplayAndDrawInContext:NO]; +} + +- (void)testDelegateDisplayAndDrawInContextAsync +{ + [self checkDelegateDisplayAndDrawInContext:YES]; +} + +- (void)testCancelAsyncDisplay +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay]; + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + + dispatch_suspend([_ASDisplayLayer displayQueue]); + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + [layer setNeedsDisplay]; + XCTAssertTrue(layer.needsDisplay); + [layer displayIfNeeded]; + + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + + [layer cancelAsyncDisplay]; + + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.displayCount, 1u); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.didDisplayCount, 0u); + XCTAssertEqual(asyncDelegate.displayCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 0u); + + [asyncDelegate release]; +} + +- (void)testTransaction +{ + _ASDisplayLayerTestDelegateMode delegateModes = _ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters; + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDisplay]; + + // Setup + _ASDisplayLayerTestContainerLayer *containerLayer = [[_ASDisplayLayerTestContainerLayer alloc] init]; + containerLayer.asyncdisplaykit_asyncTransactionContainer = YES; + containerLayer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + + _ASDisplayLayerTestDelegate *layer1Delegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:delegateModes]; + _ASDisplayLayerTestLayer *layer1 = (_ASDisplayLayerTestLayer *)layer1Delegate.layer; + layer1.displaysAsynchronously = YES; + + dispatch_semaphore_t displayAsyncLayer1Sema = dispatch_semaphore_create(0); + layer1Delegate.displayLayerBlock = ^(_ASDisplayLayer *asyncLayer) { + dispatch_semaphore_wait(displayAsyncLayer1Sema, DISPATCH_TIME_FOREVER); + return bogusImage(); + }; + layer1.backgroundColor = [UIColor blackColor].CGColor; + layer1.frame = CGRectMake(0.0, 0.0, 333.0, 123.0); + [containerLayer addSublayer:layer1]; + + _ASDisplayLayerTestDelegate *layer2Delegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:delegateModes]; + _ASDisplayLayerTestLayer *layer2 = (_ASDisplayLayerTestLayer *)layer2Delegate.layer; + layer2.displaysAsynchronously = YES; + layer2.backgroundColor = [UIColor blackColor].CGColor; + layer2.frame = CGRectMake(0.0, 50.0, 97.0, 50.0); + [containerLayer addSublayer:layer2]; + + dispatch_suspend([_ASDisplayLayer displayQueue]); + + // display below if needed + [layer1 setNeedsDisplay]; + [layer2 setNeedsDisplay]; + [containerLayer setNeedsDisplay]; + [self displayLayerRecursively:containerLayer]; + + // check state before running displayQueue + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + XCTAssertEqual(containerLayer.didCompleteTransactionCount, 0u); + + // run displayQueue until async display for layer2 has been run + dispatch_resume([_ASDisplayLayer displayQueue]); + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (layer2Delegate.displayCount == 1); + })); + + // check layer1 has not had async display run + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 0u); + // check layer2 has had async display run + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer2Delegate.displayCount, 1u); + XCTAssertEqual(containerLayer.didCompleteTransactionCount, 0u); + + + // allow layer1 to complete display + dispatch_semaphore_signal(displayAsyncLayer1Sema); + [self waitForLayer:layer1 asyncDisplayCount:1]; + + // check that both layers have completed display + XCTAssertTrue([layer1 checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer1.setContentsCounts); + XCTAssertEqual(layer1.displayCount, 1u); + XCTAssertEqual(layer1Delegate.displayCount, 1u); + XCTAssertTrue([layer2 checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer2.setContentsCounts); + XCTAssertEqual(layer2.displayCount, 1u); + XCTAssertEqual(layer2Delegate.displayCount, 1u); + + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ + return (containerLayer.didCompleteTransactionCount == 1); + })); + + [containerLayer release]; + dispatch_release(displayAsyncLayer1Sema); +} + +- (void)checkSuspendResume:(BOOL)displaysAsynchronously +{ + [_ASDisplayLayerTestDelegate setClassModes:_ASDisplayLayerTestDelegateClassModeDrawInContext]; + _ASDisplayLayerTestDelegate *asyncDelegate = [[_ASDisplayLayerTestDelegate alloc] initWithModes:_ASDisplayLayerTestDelegateModeDidDisplay | _ASDisplayLayerTestDelegateModeDrawParameters]; + + _ASDisplayLayerTestLayer *layer = (_ASDisplayLayerTestLayer *)asyncDelegate.layer; + layer.displaysAsynchronously = displaysAsynchronously; + layer.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); + + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + + // Layer shouldn't display because display is suspended + layer.displaySuspended = YES; + [layer setNeedsDisplay]; + [layer displayIfNeeded]; + XCTAssertEqual(layer.displayCount, 0u, @"Should not have displayed because display is suspended, thus -setNeedsDisplay is a no-op"); + XCTAssertFalse(layer.needsDisplay, @"Should not need display"); + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForDisplayQueue]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + } + XCTAssertFalse(layer.needsDisplay); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + + // Layer should display because display is resumed + if (displaysAsynchronously) { + dispatch_suspend([_ASDisplayLayer displayQueue]); + } + layer.displaySuspended = NO; + XCTAssertTrue(layer.needsDisplay); + [layer displayIfNeeded]; + XCTAssertEqual(layer.displayCount, 1u); + if (displaysAsynchronously) { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:0], @"%@", layer.setContentsCounts); + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawRectCount, 0u); + dispatch_resume([_ASDisplayLayer displayQueue]); + [self waitForLayer:layer asyncDisplayCount:1]; + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:0 asyncCount:1], @"%@", layer.setContentsCounts); + } else { + XCTAssertTrue([layer checkSetContentsCountsWithSyncCount:1 asyncCount:0], @"%@", layer.setContentsCounts); + } + XCTAssertEqual(layer.drawInContextCount, 0u); + XCTAssertEqual(asyncDelegate.drawParametersCount, 1u); + XCTAssertEqual(asyncDelegate.drawRectCount, 1u); + XCTAssertFalse(layer.needsDisplay); + + [asyncDelegate release]; +} + +- (void)testSuspendResumeAsync +{ + [self checkSuspendResume:YES]; +} + +- (void)testSuspendResumeSync +{ + [self checkSuspendResume:NO]; +} + +@end diff --git a/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m new file mode 100644 index 0000000000..fa418acc05 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeAppearanceTests.m @@ -0,0 +1,450 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import + +#import "_ASDisplayView.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNodeExtras.h" +#import "UIView+ASConvenience.h" + +// helper functions +IMP class_replaceMethodWithBlock(Class class, SEL originalSelector, id block); +IMP class_replaceMethodWithBlock(Class class, SEL originalSelector, id block) +{ + IMP newImplementation = imp_implementationWithBlock(block); + Method method = class_getInstanceMethod(class, originalSelector); + return class_replaceMethod(class, originalSelector, newImplementation, method_getTypeEncoding(method)); +} + +static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(Class class, SEL originalSelector, id block); +static dispatch_block_t modifyMethodByAddingPrologueBlockAndReturnCleanupBlock(Class class, SEL originalSelector, id block) +{ + __block IMP originalImp = NULL; + void (^blockCopied)(id) = [block copy]; + void (^blockActualSwizzle)(id) = [^(id swizzedSelf){ + blockCopied(swizzedSelf); + originalImp(swizzedSelf, originalSelector); + } copy]; + originalImp = class_replaceMethodWithBlock(class, originalSelector, blockActualSwizzle); + void (^cleanupBlock)(void) = ^{ + // restore original method + Method method = class_getInstanceMethod(class, originalSelector); + class_replaceMethod(class, originalSelector, originalImp, method_getTypeEncoding(method)); + // release copied blocks + [blockCopied release]; + [blockActualSwizzle release]; + }; + return [[cleanupBlock copy] autorelease]; +} + +@interface ASDisplayNode (PrivateStuffSoWeDontPullInCPPInternalH) +- (BOOL)__visibilityNotificationsDisabled; +@end + +@interface ASDisplayNodeAppearanceTests : XCTestCase +@end + +#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n +#define DeclareViewNamed(v) UIView *v = [[UIView alloc] init]; v.layer.asyncdisplaykit_name = @#v +#define DeclareLayerNamed(l) CALayer *l = [[CALayer alloc] init]; l.asyncdisplaykit_name = @#l + +@implementation ASDisplayNodeAppearanceTests +{ + _ASDisplayView *_view; + + NSMutableArray *_swizzleCleanupBlocks; + + NSCountedSet *_willAppearCounts; + NSCountedSet *_willDisappearCounts; + NSCountedSet *_didDisappearCounts; + +} + +- (void)setUp +{ + [super setUp]; + + _swizzleCleanupBlocks = [[NSMutableArray alloc] init]; + + // Using this instead of mocks. Count # of times method called + _willAppearCounts = [[NSCountedSet alloc] init]; + _willDisappearCounts = [[NSCountedSet alloc] init]; + _didDisappearCounts = [[NSCountedSet alloc] init]; + + dispatch_block_t cleanupBlock = modifyMethodByAddingPrologueBlockAndReturnCleanupBlock([ASDisplayNode class], @selector(willAppear), ^(id blockSelf){ + [_willAppearCounts addObject:blockSelf]; + }); + [_swizzleCleanupBlocks addObject:cleanupBlock]; + cleanupBlock = modifyMethodByAddingPrologueBlockAndReturnCleanupBlock([ASDisplayNode class], @selector(didDisappear), ^(id blockSelf){ + [_didDisappearCounts addObject:blockSelf]; + }); + [_swizzleCleanupBlocks addObject:cleanupBlock]; + cleanupBlock = modifyMethodByAddingPrologueBlockAndReturnCleanupBlock([ASDisplayNode class], @selector(willDisappear), ^(id blockSelf){ + [_willDisappearCounts addObject:blockSelf]; + }); + [_swizzleCleanupBlocks addObject:cleanupBlock]; +} + +- (void)tearDown +{ + [super tearDown]; + + for(id cleanupBlock in _swizzleCleanupBlocks) { + void (^cleanupBlockCasted)(void) = cleanupBlock; + cleanupBlockCasted(); + } + [_swizzleCleanupBlocks release]; + _swizzleCleanupBlocks = nil; + + [_willAppearCounts release]; + _willAppearCounts = nil; + [_willDisappearCounts release]; + _willDisappearCounts = nil; + [_didDisappearCounts release]; + _didDisappearCounts = nil; +} + +- (void)testAppearanceMethodsCalledWithRootNodeInWindowLayer +{ + [self checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:YES]; +} + +- (void)testAppearanceMethodsCalledWithRootNodeInWindowView +{ + [self checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:NO]; +} + +- (void)checkAppearanceMethodsCalledWithRootNodeInWindowLayerBacked:(BOOL)isLayerBacked +{ + // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(n); + DeclareViewNamed(superview); + + n.isLayerBacked = isLayerBacked; + + if (isLayerBacked) { + [superview.layer addSublayer:n.layer]; + } else { + [superview addSubview:n.view]; + } + + XCTAssertEqual([_willAppearCounts countForObject:n], 0u, @"willAppear erroneously called"); + XCTAssertEqual([_willDisappearCounts countForObject:n], 0u, @"willDisppear erroneously called"); + XCTAssertEqual([_didDisappearCounts countForObject:n], 0u, @"didDisappear erroneously called"); + + [window addSubview:superview]; + XCTAssertEqual([_willAppearCounts countForObject:n], 1u, @"willAppear not called when node's view added to hierarchy"); + XCTAssertEqual([_willDisappearCounts countForObject:n], 0u, @"willDisppear erroneously called"); + XCTAssertEqual([_didDisappearCounts countForObject:n], 0u, @"didDisappear erroneously called"); + + XCTAssertTrue(n.inWindow, @"Node should be visible"); + + if (isLayerBacked) { + [n.layer removeFromSuperlayer]; + } else { + [n.view removeFromSuperview]; + } + + XCTAssertFalse(n.inWindow, @"Node should be not visible"); + + XCTAssertEqual([_willAppearCounts countForObject:n], 1u, @"willAppear not called when node's view added to hierarchy"); + XCTAssertEqual([_willDisappearCounts countForObject:n], 1u, @"willDisppear erroneously called"); + XCTAssertEqual([_didDisappearCounts countForObject:n], 1u, @"didDisappear erroneously called"); + + [superview release]; + [window release]; +} + +- (void)checkManualAppearanceViewLoaded:(BOOL)isViewLoaded layerBacked:(BOOL)isLayerBacked +{ + // ASDisplayNode visibility does not change if modifying a hierarchy that is not in a window. So create one and add the superview to it. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(aa); + DeclareNodeNamed(ab); + + for (ASDisplayNode *n in @[parent, a, b, aa, ab]) { + n.isLayerBacked = isLayerBacked; + if (isViewLoaded) + [n layer]; + } + + [parent addSubnode:a]; + + XCTAssertFalse(parent.inWindow, @"Nothing should be visible"); + XCTAssertFalse(a.inWindow, @"Nothing should be visible"); + XCTAssertFalse(b.inWindow, @"Nothing should be visible"); + XCTAssertFalse(aa.inWindow, @"Nothing should be visible"); + XCTAssertFalse(ab.inWindow, @"Nothing should be visible"); + + if (isLayerBacked) { + [window.layer addSublayer:parent.layer]; + } else { + [window addSubview:parent.view]; + } + + XCTAssertEqual([_willAppearCounts countForObject:parent], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:a], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:b], 0u, @"Should not have appeared yet"); + XCTAssertEqual([_willAppearCounts countForObject:aa], 0u, @"Should not have appeared yet"); + XCTAssertEqual([_willAppearCounts countForObject:ab], 0u, @"Should not have appeared yet"); + + XCTAssertTrue(parent.inWindow, @"Should be visible"); + XCTAssertTrue(a.inWindow, @"Should be visible"); + XCTAssertFalse(b.inWindow, @"Nothing should be visible"); + XCTAssertFalse(aa.inWindow, @"Nothing should be visible"); + XCTAssertFalse(ab.inWindow, @"Nothing should be visible"); + + // Add to an already-visible node should make the node visible + [parent addSubnode:b]; + [a insertSubnode:aa atIndex:0]; + [a insertSubnode:ab aboveSubnode:aa]; + + XCTAssertTrue(parent.inWindow, @"Should be visible"); + XCTAssertTrue(a.inWindow, @"Should be visible"); + XCTAssertTrue(b.inWindow, @"Should be visible after adding to visible parent"); + XCTAssertTrue(aa.inWindow, @"Nothing should be visible"); + XCTAssertTrue(ab.inWindow, @"Nothing should be visible"); + + XCTAssertEqual([_willAppearCounts countForObject:parent], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:a], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:b], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:aa], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:ab], 1u, @"Should have -willAppear called once"); + + if (isLayerBacked) { + [parent.layer removeFromSuperlayer]; + } else { + [parent.view removeFromSuperview]; + } + + XCTAssertEqual([_willDisappearCounts countForObject:parent], 1u, @"Should disappear properly"); + XCTAssertEqual([_willDisappearCounts countForObject:a], 1u, @"Should disappear properly"); + XCTAssertEqual([_willDisappearCounts countForObject:b], 1u, @"Should disappear properly"); + XCTAssertEqual([_willDisappearCounts countForObject:aa], 1u, @"Should disappear properly"); + XCTAssertEqual([_willDisappearCounts countForObject:ab], 1u, @"Should disappear properly"); + + XCTAssertFalse(parent.inWindow, @"Nothing should be visible"); + XCTAssertFalse(a.inWindow, @"Nothing should be visible"); + XCTAssertFalse(b.inWindow, @"Nothing should be visible"); + XCTAssertFalse(aa.inWindow, @"Nothing should be visible"); + XCTAssertFalse(ab.inWindow, @"Nothing should be visible"); +} + +- (void)testAppearanceMethodsNoLayer +{ + [self checkManualAppearanceViewLoaded:NO layerBacked:YES]; +} + +- (void)testAppearanceMethodsNoView +{ + [self checkManualAppearanceViewLoaded:NO layerBacked:NO]; +} + +- (void)testAppearanceMethodsLayer +{ + [self checkManualAppearanceViewLoaded:YES layerBacked:YES]; +} + +- (void)testAppearanceMethodsView +{ + [self checkManualAppearanceViewLoaded:YES layerBacked:NO]; +} + +- (void)testSynchronousIntermediaryView +{ + // Parent is a wrapper node for a scrollview + ASDisplayNode *parentSynchronousNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + DeclareNodeNamed(layerBackedNode); + DeclareNodeNamed(viewBackedNode); + + layerBackedNode.isLayerBacked = YES; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + [parentSynchronousNode addSubnode:layerBackedNode]; + [parentSynchronousNode addSubnode:viewBackedNode]; + + XCTAssertFalse(parentSynchronousNode.inWindow, @"Should not yet be visible"); + XCTAssertFalse(layerBackedNode.inWindow, @"Should not yet be visible"); + XCTAssertFalse(viewBackedNode.inWindow, @"Should not yet be visible"); + + [window addSubview:parentSynchronousNode.view]; + + // This is a known case that isn't supported + XCTAssertFalse(parentSynchronousNode.inWindow, @"Synchronous views are not currently marked visible"); + + XCTAssertTrue(layerBackedNode.inWindow, @"Synchronous views' subviews should get marked visible"); + XCTAssertTrue(viewBackedNode.inWindow, @"Synchronous views' subviews should get marked visible"); + + // Try moving a node to/from a synchronous node in the window with the node API + // Setup + [layerBackedNode removeFromSupernode]; + [viewBackedNode removeFromSupernode]; + XCTAssertFalse(layerBackedNode.inWindow, @"aoeu"); + XCTAssertFalse(viewBackedNode.inWindow, @"aoeu"); + + // now move to synchronous node + [parentSynchronousNode addSubnode:layerBackedNode]; + [parentSynchronousNode insertSubnode:viewBackedNode aboveSubnode:layerBackedNode]; + XCTAssertTrue(layerBackedNode.inWindow, @"Synchronous views' subviews should get marked visible"); + XCTAssertTrue(viewBackedNode.inWindow, @"Synchronous views' subviews should get marked visible"); + + [parentSynchronousNode.view removeFromSuperview]; + + XCTAssertFalse(parentSynchronousNode.inWindow, @"Should not have changed"); + XCTAssertFalse(layerBackedNode.inWindow, @"Should have been marked invisible when synchronous superview was removed from the window"); + XCTAssertFalse(viewBackedNode.inWindow, @"Should have been marked invisible when synchronous superview was removed from the window"); + + [window release]; + [parentSynchronousNode release]; + [layerBackedNode release]; + [viewBackedNode release]; +} + +- (void)checkMoveAcrossHierarchyLayerBacked:(BOOL)isLayerBacked useManualCalls:(BOOL)useManualDisable useNodeAPI:(BOOL)useNodeAPI +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectZero]; + + DeclareNodeNamed(parentA); + DeclareNodeNamed(parentB); + DeclareNodeNamed(child); + DeclareNodeNamed(childSubnode); + + for (ASDisplayNode *n in @[parentA, parentB, child, childSubnode]) { + n.isLayerBacked = isLayerBacked; + } + + [parentA addSubnode:child]; + [child addSubnode:childSubnode]; + + XCTAssertFalse(parentA.inWindow, @"Should not yet be visible"); + XCTAssertFalse(parentB.inWindow, @"Should not yet be visible"); + XCTAssertFalse(child.inWindow, @"Should not yet be visible"); + XCTAssertFalse(childSubnode.inWindow, @"Should not yet be visible"); + XCTAssertFalse(childSubnode.inWindow, @"Should not yet be visible"); + + XCTAssertEqual([_willAppearCounts countForObject:child], 0u, @"Should not have -willAppear called"); + XCTAssertEqual([_willAppearCounts countForObject:childSubnode], 0u, @"Should not have -willAppear called"); + + if (isLayerBacked) { + [window.layer addSublayer:parentA.layer]; + [window.layer addSublayer:parentB.layer]; + } else { + [window addSubview:parentA.view]; + [window addSubview:parentB.view]; + } + + XCTAssertTrue(parentA.inWindow, @"Should be visible after added to window"); + XCTAssertTrue(parentB.inWindow, @"Should be visible after added to window"); + XCTAssertTrue(child.inWindow, @"Should be visible after parent added to window"); + XCTAssertTrue(childSubnode.inWindow, @"Should be visible after parent added to window"); + + XCTAssertEqual([_willAppearCounts countForObject:child], 1u, @"Should have -willAppear called once"); + XCTAssertEqual([_willAppearCounts countForObject:childSubnode], 1u, @"Should have -willAppear called once"); + + // Move subnode from A to B + if (useManualDisable) { + ASDisplayNodeDisableHierarchyNotifications(child); + } + if (!useNodeAPI) { + [child removeFromSupernode]; + [parentB addSubnode:child]; + } else { + [parentB addSubnode:child]; + } + if (useManualDisable) { + XCTAssertTrue([child __visibilityNotificationsDisabled], @"Should not have re-enabled yet"); + ASDisplayNodeEnableHierarchyNotifications(child); + } + + XCTAssertEqual([_willAppearCounts countForObject:child], 1u, @"Should not have -willAppear called when moving child around in hierarchy"); + XCTAssertEqual([_willDisappearCounts countForObject:child], 0u, @"Should not have -willDisappear called when moving child around in hierarchy"); + XCTAssertEqual([_willDisappearCounts countForObject:childSubnode], 0u, @"Subnode should not have -willDisappear called when moving parent (child) around in hierarchy"); + + // Move subnode back to A + if (useManualDisable) { + ASDisplayNodeDisableHierarchyNotifications(child); + } + if (!useNodeAPI) { + [child removeFromSupernode]; + [parentA insertSubnode:child atIndex:0]; + } else { + [parentA insertSubnode:child atIndex:0]; + } + if (useManualDisable) { + XCTAssertTrue([child __visibilityNotificationsDisabled], @"Should not have re-enabled yet"); + ASDisplayNodeEnableHierarchyNotifications(child); + } + + + XCTAssertEqual([_willAppearCounts countForObject:child], 1u, @"Should not have -willAppear called when moving child around in hierarchy"); + XCTAssertEqual([_willDisappearCounts countForObject:child], 0u, @"Should not have -willDisappear called when moving child around in hierarchy"); + + // Finally, remove subnode + [child removeFromSupernode]; + + XCTAssertEqual([_willAppearCounts countForObject:child], 1u, @"Should appear and disappear just once"); + XCTAssertEqual([_willDisappearCounts countForObject:child], 1u, @"Should appear and disappear just once"); + XCTAssertEqual([_willDisappearCounts countForObject:childSubnode], 1u, @"Should appear and disappear just once"); + + // Make sure that we don't leave these unbalanced + XCTAssertFalse([child __visibilityNotificationsDisabled], @"Unbalanced visibility notifications calls"); + + [window release]; +} + +- (void)testMoveAcrossHierarchyLayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:NO useNodeAPI:YES]; +} + +- (void)testMoveAcrossHierarchyView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:NO useNodeAPI:YES]; +} + +- (void)testMoveAcrossHierarchyManualLayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:YES useNodeAPI:NO]; +} + +- (void)testMoveAcrossHierarchyManualView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:YES useNodeAPI:NO]; +} + +- (void)testDisableWithNodeAPILayer +{ + [self checkMoveAcrossHierarchyLayerBacked:YES useManualCalls:YES useNodeAPI:YES]; +} + +- (void)testDisableWithNodeAPIView +{ + [self checkMoveAcrossHierarchyLayerBacked:NO useManualCalls:YES useNodeAPI:YES]; +} + +- (void)testPreventManualAppearanceMethods +{ + DeclareNodeNamed(n); + + XCTAssertThrows([n willAppear], @"Should not allow manually calling appearance methods."); + XCTAssertThrows([n willDisappear], @"Should not allow manually calling appearance methods."); + XCTAssertThrows([n didDisappear], @"Should not allow manually calling appearance methods."); +} + +@end diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m new file mode 100644 index 0000000000..53525a1d81 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -0,0 +1,1639 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "_ASDisplayLayer.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNodeTestsHelper.h" +#import "UIView+ASConvenience.h" + +// Conveniences for making nodes named a certain way + +#define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.name = @#n +#define DeclareViewNamed(v) UIView *v = [[UIView alloc] init]; v.layer.asyncdisplaykit_name = @#v +#define DeclareLayerNamed(l) CALayer *l = [[CALayer alloc] init]; l.asyncdisplaykit_name = @#l + +static NSString *orderStringFromSublayers(CALayer *l) { + return [[[l.sublayers valueForKey:@"asyncdisplaykit_name"] allObjects] componentsJoinedByString:@","]; +} + +static NSString *orderStringFromSubviews(UIView *v) { + return [[[v.subviews valueForKeyPath:@"layer.asyncdisplaykit_name"] allObjects] componentsJoinedByString:@","]; +} + +static NSString *orderStringFromSubnodes(ASDisplayNode *n) { + return [[[n.subnodes valueForKey:@"name"] allObjects] componentsJoinedByString:@","]; +} + +// Asserts subnode, subview, sublayer order match what you provide here +#define XCTAssertNodeSubnodeSubviewSublayerOrder(n, loaded, isLayerBacked, order, description) \ +XCTAssertEqualObjects(orderStringFromSubnodes(n), order, @"Incorrect node order for " description );\ +if (loaded) {\ + if (!isLayerBacked) {\ + XCTAssertEqualObjects(orderStringFromSubviews(n.view), order, @"Incorrect subviews for " description);\ + }\ + XCTAssertEqualObjects(orderStringFromSublayers(n.layer), order, @"Incorrect sublayers for " description);\ +} + +#define XCTAssertNodesHaveParent(parent, nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertEqualObjects(parent, n.supernode, @"%@ has the wrong parent", n.name);\ +} + +#define XCTAssertNodesLoaded(nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertTrue(n.isViewLoaded, @"%@ should be loaded", n.name);\ +} + +#define XCTAssertNodesNotLoaded(nodes ...) \ +for (ASDisplayNode *n in @[ nodes ]) {\ + XCTAssertFalse(n.isViewLoaded, @"%@ should not be loaded", n.name);\ +} + + +@interface ASDisplayNode (HackForTests) + ++ (dispatch_queue_t)asyncSizingQueue; + +@end + +@interface ASTestDisplayNode : ASDisplayNode +@property (atomic, copy) void (^willDeallocBlock)(ASTestDisplayNode *node); +@property (atomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); +@end + +@implementation ASTestDisplayNode + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + return _calculateSizeBlock ? _calculateSizeBlock(self, constrainedSize) : CGSizeZero; +} + +- (void)dealloc +{ + if (_willDeallocBlock) { + _willDeallocBlock(self); + } + [super dealloc]; +} + +@end + +@interface ASDisplayNodeTests : XCTestCase +@end + +@implementation ASDisplayNodeTests +{ + dispatch_queue_t queue; +} + +- (void)setUp +{ + [super setUp]; + queue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDisplayNodeTestsQueue", NULL); +} + +- (void)tearDown +{ + dispatch_release(queue); + [super tearDown]; +} + +- (void)testViewCreatedOffThreadCanBeRealizedOnThread +{ + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + }]; + + UIView *view = node.view; + XCTAssertNotNil(view, @"Getting node's view on-thread should succeed."); +} + +- (void)checkValuesMatchDefaults:(ASDisplayNode *)node isLayerBacked:(BOOL)isLayerBacked +{ + NSString *targetName = isLayerBacked ? @"layer" : @"view"; + NSString *hasLoadedView = node.isViewLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; + + id rgbBlackCGColorIdPtr = (id)[UIColor colorWithRed:0 green:0 blue:0 alpha:1].CGColor; + + XCTAssertEqual((id)nil, node.contents, @"default contents broken %@", hasLoadedView); + XCTAssertEqual(NO, node.clipsToBounds, @"default clipsToBounds broken %@", hasLoadedView); + XCTAssertEqual(YES, node.opaque, @"default opaque broken %@", hasLoadedView); + XCTAssertEqual(NO, node.needsDisplayOnBoundsChange, @"default needsDisplayOnBoundsChange broken %@", hasLoadedView); + XCTAssertEqual(NO, node.allowsEdgeAntialiasing, @"default allowsEdgeAntialiasing broken %@", hasLoadedView); + XCTAssertEqual((unsigned int)(kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge), node.edgeAntialiasingMask, @"default edgeAntialisingMask broken %@", hasLoadedView); + XCTAssertEqual(NO, node.hidden, @"default hidden broken %@", hasLoadedView); + XCTAssertEqual(1.0f, node.alpha, @"default alpha broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.bounds), @"default bounds broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.frame), @"default frame broken %@", hasLoadedView); + XCTAssertTrue(CGPointEqualToPoint(CGPointZero, node.position), @"default position broken %@", hasLoadedView); + XCTAssertEqual((CGFloat)0.0, node.zPosition, @"default zPosition broken %@", hasLoadedView); + XCTAssertEqual(1.0f, node.contentsScale, @"default contentsScale broken %@", hasLoadedView); + XCTAssertEqual([UIScreen mainScreen].scale, node.contentsScaleForDisplay, @"default contentsScaleForDisplay broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.transform), @"default transform broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.subnodeTransform), @"default subnodeTransform broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.backgroundColor, @"default backgroundColor broken %@", hasLoadedView); + XCTAssertEqual(UIViewContentModeScaleToFill, node.contentMode, @"default contentMode broken %@", hasLoadedView); + XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); + XCTAssertEqual(0.0f, node.shadowOpacity, @"default shadowOpacity broken %@", hasLoadedView); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(0, -3), node.shadowOffset), @"default shadowOffset broken %@", hasLoadedView); + XCTAssertEqual(3.f, node.shadowRadius, @"default shadowRadius broken %@", hasLoadedView); + XCTAssertEqual(0.0f, node.borderWidth, @"default borderWidth broken %@", hasLoadedView); + XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); + XCTAssertEqual(NO, node.preventOrCancelDisplay, @"default preventOrCancelDisplay broken %@", hasLoadedView); + XCTAssertEqual(YES, node.displaysAsynchronously, @"default displaysAsynchronously broken %@", hasLoadedView); + XCTAssertEqual(NO, node.asyncdisplaykit_asyncTransactionContainer, @"default asyncdisplaykit_asyncTransactionContainer broken %@", hasLoadedView); + XCTAssertEqualObjects(nil, node.name, @"default name broken %@", hasLoadedView); + + if (!isLayerBacked) { + XCTAssertEqual(YES, node.userInteractionEnabled, @"default userInteractionEnabled broken %@", hasLoadedView); + XCTAssertEqual(NO, node.exclusiveTouch, @"default exclusiveTouch broken %@", hasLoadedView); + XCTAssertEqual(YES, node.autoresizesSubviews, @"default autoresizesSubviews broken %@", hasLoadedView); + XCTAssertEqual(UIViewAutoresizingNone, node.autoresizingMask, @"default autoresizingMask broken %@", hasLoadedView); + } else { + XCTAssertEqual(NO, node.userInteractionEnabled, @"layer-backed nodes do not support userInteractionEnabled %@", hasLoadedView); + XCTAssertEqual(NO, node.exclusiveTouch, @"layer-backed nodes do not support exclusiveTouch %@", hasLoadedView); + } + + if (!isLayerBacked) { + XCTAssertEqual(NO, node.isAccessibilityElement, @"default isAccessibilityElement is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityLabel, @"default accessibilityLabel is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityHint, @"default accessibilityHint is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityValue, @"default accessibilityValue is broken %@", hasLoadedView); + XCTAssertEqual(UIAccessibilityTraitNone, node.accessibilityTraits, @"default accessibilityTraits is broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.accessibilityFrame), @"default accessibilityFrame is broken %@", hasLoadedView); + XCTAssertEqual((id)nil, node.accessibilityLanguage, @"default accessibilityLanguage is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.accessibilityElementsHidden, @"default accessibilityElementsHidden is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.accessibilityViewIsModal, @"default accessibilityViewIsModal is broken %@", hasLoadedView); + XCTAssertEqual(NO, node.shouldGroupAccessibilityChildren, @"default shouldGroupAccessibilityChildren is broken %@", hasLoadedView); + } +} + +- (void)checkDefaultPropertyValuesWithLayerBacking:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + XCTAssertEqual(NO, node.isLayerBacked, @"default isLayerBacked broken without view"); + node.isLayerBacked = isLayerBacked; + XCTAssertEqual(isLayerBacked, node.isLayerBacked, @"setIsLayerBacked: broken"); + + // Assert that the values can be fetched from the node before the view is realized. + [self checkValuesMatchDefaults:node isLayerBacked:isLayerBacked]; + + [node layer]; // Force either view or layer loading + XCTAssertTrue(node.isViewLoaded, @"Didn't load view"); + + // Assert that the values can be fetched from the node after the view is realized. + [self checkValuesMatchDefaults:node isLayerBacked:isLayerBacked]; +} + +- (void)testDefaultPropertyValuesLayer +{ + [self checkDefaultPropertyValuesWithLayerBacking:YES]; +} + +- (void)testDefaultPropertyValuesView +{ + [self checkDefaultPropertyValuesWithLayerBacking:NO]; +} + +- (UIImage *)bogusImage +{ + static UIImage *bogusImage; + if (!bogusImage) { + UIGraphicsBeginImageContext(CGSizeMake(1, 1)); + bogusImage = [UIGraphicsGetImageFromCurrentImageContext() retain]; + UIGraphicsEndImageContext(); + } + return bogusImage; +} + +- (void)checkValuesMatchSetValues:(ASDisplayNode *)node isLayerBacked:(BOOL)isLayerBacked +{ + NSString *targetName = isLayerBacked ? @"layer" : @"view"; + NSString *hasLoadedView = node.isViewLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; + + XCTAssertEqual(isLayerBacked, node.isLayerBacked, @"isLayerBacked broken %@", hasLoadedView); + XCTAssertEqualObjects((id)[self bogusImage].CGImage, (id)node.contents, @"contents broken %@", hasLoadedView); + XCTAssertEqual(YES, node.clipsToBounds, @"clipsToBounds broken %@", hasLoadedView); + XCTAssertEqual(NO, node.opaque, @"opaque broken %@", hasLoadedView); + XCTAssertEqual(YES, node.needsDisplayOnBoundsChange, @"needsDisplayOnBoundsChange broken %@", hasLoadedView); + XCTAssertEqual(YES, node.allowsEdgeAntialiasing, @"allowsEdgeAntialiasing broken %@", hasLoadedView); + XCTAssertTrue((unsigned int)(kCALayerLeftEdge | kCALayerTopEdge) == node.edgeAntialiasingMask, @"edgeAntialiasingMask broken: %@", hasLoadedView); + XCTAssertEqual(YES, node.hidden, @"hidden broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.alpha, @"alpha broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 15, 42, 115.2), node.bounds), @"bounds broken %@", hasLoadedView); + XCTAssertTrue(CGPointEqualToPoint(CGPointMake(10, 65), node.position), @"position broken %@", hasLoadedView); + XCTAssertEqual((CGFloat)5.6, node.zPosition, @"zPosition broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.contentsScale, @"contentsScale broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DMakeScale(0.5, 0.5, 1.0), node.transform), @"transform broken %@", hasLoadedView); + XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DMakeTranslation(1337, 7357, 7007), node.subnodeTransform), @"subnodeTransform broken %@", hasLoadedView); + XCTAssertEqualObjects([UIColor clearColor], node.backgroundColor, @"backgroundColor broken %@", hasLoadedView); + XCTAssertEqual(UIViewContentModeBottom, node.contentMode, @"contentMode broken %@", hasLoadedView); + XCTAssertEqual([[UIColor cyanColor] CGColor], node.shadowColor, @"shadowColor broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.shadowOpacity, @"shadowOpacity broken %@", hasLoadedView); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(1.0f, 1.0f), node.shadowOffset), @"shadowOffset broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.shadowRadius, @"shadowRadius broken %@", hasLoadedView); + XCTAssertEqual(.5f, node.borderWidth, @"borderWidth broken %@", hasLoadedView); + XCTAssertEqual([[UIColor orangeColor] CGColor], node.borderColor, @"borderColor broken %@", hasLoadedView); + XCTAssertEqual(YES, node.preventOrCancelDisplay, @"preventOrCancelDisplay broken %@", hasLoadedView); + XCTAssertEqual(NO, node.displaysAsynchronously, @"preventOrCancelDisplay broken %@", hasLoadedView); + XCTAssertEqual(YES, node.asyncdisplaykit_asyncTransactionContainer, @"asyncTransactionContainer broken %@", hasLoadedView); + XCTAssertEqual(NO, node.userInteractionEnabled, @"userInteractionEnabled broken %@", hasLoadedView); + XCTAssertEqual((BOOL)!isLayerBacked, node.exclusiveTouch, @"exclusiveTouch broken %@", hasLoadedView); + XCTAssertEqualObjects(@"quack like a duck", node.name, @"name broken %@", hasLoadedView); + + if (!isLayerBacked) { + XCTAssertEqual(UIViewAutoresizingFlexibleLeftMargin, node.autoresizingMask, @"autoresizingMask %@", hasLoadedView); + XCTAssertEqual(NO, node.autoresizesSubviews, @"autoresizesSubviews broken %@", hasLoadedView); + XCTAssertEqual(YES, node.isAccessibilityElement, @"accessibilityElement broken %@", hasLoadedView); + XCTAssertEqualObjects(@"Ship love", node.accessibilityLabel, @"accessibilityLabel broken %@", hasLoadedView); + XCTAssertEqualObjects(@"Awesome things will happen", node.accessibilityHint, @"accessibilityHint broken %@", hasLoadedView); + XCTAssertEqualObjects(@"1 of 2", node.accessibilityValue, @"accessibilityValue broken %@", hasLoadedView); + XCTAssertEqual(UIAccessibilityTraitSelected | UIAccessibilityTraitButton, node.accessibilityTraits, @"accessibilityTraits broken %@", hasLoadedView); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(1, 2, 3, 4), node.accessibilityFrame), @"accessibilityFrame broken %@", hasLoadedView); + XCTAssertEqualObjects(@"mas", node.accessibilityLanguage, @"accessibilityLanguage broken %@", hasLoadedView); + XCTAssertEqual(YES, node.accessibilityElementsHidden, @"accessibilityElementsHidden broken %@", hasLoadedView); + XCTAssertEqual(YES, node.accessibilityViewIsModal, @"accessibilityViewIsModal broken %@", hasLoadedView); + XCTAssertEqual(YES, node.shouldGroupAccessibilityChildren, @"shouldGroupAccessibilityChildren broken %@", hasLoadedView); + } +} + +- (void)checkSimpleBridgePropertiesSetPropagate:(BOOL)isLayerBacked +{ + __block ASDisplayNode *node = nil; + + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.isLayerBacked = isLayerBacked; + + node.contents = (id)[self bogusImage].CGImage; + node.clipsToBounds = YES; + node.opaque = NO; + node.needsDisplayOnBoundsChange = YES; + node.allowsEdgeAntialiasing = YES; + node.edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerTopEdge); + node.hidden = YES; + node.alpha = .5f; + node.position = CGPointMake(10, 65); + node.zPosition = 5.6; + node.bounds = CGRectMake(10, 15, 42, 115.2); + node.contentsScale = .5f; + node.transform = CATransform3DMakeScale(0.5, 0.5, 1.0); + node.subnodeTransform = CATransform3DMakeTranslation(1337, 7357, 7007); + node.backgroundColor = [UIColor clearColor]; + node.contentMode = UIViewContentModeBottom; + node.shadowColor = [[UIColor cyanColor] CGColor]; + node.shadowOpacity = .5f; + node.shadowOffset = CGSizeMake(1.0f, 1.0f); + node.shadowRadius = .5f; + node.borderWidth = .5f; + node.borderColor = [[UIColor orangeColor] CGColor]; + node.preventOrCancelDisplay = YES; + node.displaysAsynchronously = NO; + node.asyncdisplaykit_asyncTransactionContainer = YES; + node.userInteractionEnabled = NO; + node.name = @"quack like a duck"; + + if (!isLayerBacked) { + node.exclusiveTouch = YES; + node.autoresizesSubviews = NO; + node.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + node.isAccessibilityElement = YES; + node.accessibilityLabel = @"Ship love"; + node.accessibilityHint = @"Awesome things will happen"; + node.accessibilityValue = @"1 of 2"; + node.accessibilityTraits = UIAccessibilityTraitSelected | UIAccessibilityTraitButton; + node.accessibilityFrame = CGRectMake(1, 2, 3, 4); + node.accessibilityLanguage = @"mas"; + node.accessibilityElementsHidden = YES; + node.accessibilityViewIsModal = YES; + node.shouldGroupAccessibilityChildren = YES; + } + }]; + + // Assert that the values can be fetched from the node before the view is realized. + [self checkValuesMatchSetValues:node isLayerBacked:isLayerBacked]; + + // Assert that the realized view/layer have the correct values. + [node layer]; + + [self checkValuesMatchSetValues:node isLayerBacked:isLayerBacked]; + + // As a final sanity check, change a value on the realized view and ensure it is fetched through the node. + if (isLayerBacked) { + node.layer.hidden = NO; + } else { + node.view.hidden = NO; + } + XCTAssertEqual(NO, node.hidden, @"After the view is realized, the node should delegate properties to the view."); +} + +// Set each of the simple bridged UIView properties to a non-default value off-thread, then +// assert that they are correct on the node and propagated to the UIView realized on-thread. +- (void)testSimpleUIViewBridgePropertiesSetOffThreadPropagate +{ + [self checkSimpleBridgePropertiesSetPropagate:NO]; +} + +- (void)testSimpleCALayerBridgePropertiesSetOffThreadPropagate +{ + [self checkSimpleBridgePropertiesSetPropagate:YES]; +} + + +// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. +- (void)testDeriveFrameFromBoundsPositionAnchorPoint +{ + UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); + plainView.layer.position = CGPointMake(10, 20); + plainView.layer.bounds = CGRectMake(0, 0, 60, 80); + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.anchorPoint = CGPointMake(0.25f, 0.75f); + node.bounds = CGRectMake(0, 0, 60, 80); + node.position = CGPointMake(10, 20); + }]; + + XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.frame), @"Node frame should match UIView frame before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.frame, node.view.frame), @"Realized view frame should match UIView frame."); +} + +// Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. +- (void)testSetFrameSetsBoundsPosition +{ + UIView *plainView = [[UIView alloc] initWithFrame:CGRectZero]; + plainView.layer.anchorPoint = CGPointMake(0.25f, 0.75f); + plainView.layer.frame = CGRectMake(10, 20, 60, 80); + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] init]; + node.anchorPoint = CGPointMake(0.25f, 0.75f); + node.frame = CGRectMake(10, 20, 60, 80); + }]; + + XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.position), @"Node position should match UIView position before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.bounds), @"Node bounds should match UIView bounds before realization."); + XCTAssertTrue(CGPointEqualToPoint(plainView.layer.position, node.view.layer.position), @"Realized view position should match UIView position before realization."); + XCTAssertTrue(CGRectEqualToRect(plainView.layer.bounds, node.view.layer.bounds), @"Realized view bounds should match UIView bounds before realization."); +} + +- (void)testDisplayNodePointConversionWithFrames +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(105, 105), correctPoint = CGPointMake(95, 95); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(15, 15); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.frame = CGRectMake(100, 100, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(95, 95), correctPoint = CGPointMake(105, 105); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.frame = CGRectMake(0, 0, 100, 100); + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(-5, -5); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Test conversions when bounds is not null. +// NOTE: Esoteric values were picked to facilitate visual inspection by demonstrating the relevance of certain numbers and lack of relevance of others +- (void)testDisplayNodePointConversionWithNonZeroBounds +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(36, 36); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(5, 5), correctPoint = CGPointMake(11, 11); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(36, 36), correctPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.anchorPoint = CGPointZero; + innerNode.anchorPoint = CGPointZero; + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(11, 11), correctPoint = CGPointMake(5, 5); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Test conversions when the anchorPoint is not {0.0, 0.0}. +- (void)testDisplayNodePointConversionWithNonZeroAnchorPoint +{ + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + + // Setup + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero, correctPoint = CGPointZero; + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* outer node's coordinate space to inner node's coordinate space + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42), correctPoint = CGPointMake(51, 56); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:node selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point *FROM* inner node's coordinate space to outer node's coordinate space + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(55, 55), correctPoint = CGPointMake(1, 1); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:node]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in inner node's coordinate space *TO* outer node's coordinate space + node.bounds = CGRectMake(20, 20, 100, 100); + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(51, 56), correctPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint toNode:node selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); + + // Setup + node = [[[ASDisplayNode alloc] init] autorelease], innerNode = [[[ASDisplayNode alloc] init] autorelease]; + [node addSubnode:innerNode]; + + // Convert point in outer node's coordinate space *TO* inner node's coordinate space + node.bounds = CGRectMake(-1000, -1000, 1337, 1337); + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(1, 1), correctPoint = CGPointMake(55, 55); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:node]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, correctPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +- (void)testDisplayNodePointConversionAgainstSelf { + ASDisplayNode *innerNode = nil; + CGPoint originalPoint = CGPointZero, convertedPoint = CGPointZero; + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(105, 105); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(convertedPoint, originalPoint, 0.001), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(42, 42); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.anchorPoint = CGPointMake(0.3, 0.3); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 200, 200); + originalPoint = CGPointMake(55, 55); + convertedPoint = [self checkConvertPoint:originalPoint fromNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.frame = CGRectMake(10, 10, 20, 20); + originalPoint = CGPointMake(95, 95); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(36, 36); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); + + innerNode = [[[ASDisplayNode alloc] init] autorelease]; + innerNode.anchorPoint = CGPointMake(0.75, 1); + innerNode.position = CGPointMake(23, 23); + innerNode.bounds = CGRectMake(17, 17, 20, 20); + originalPoint = CGPointMake(51, 56); + convertedPoint = [self checkConvertPoint:originalPoint toNode:innerNode selfNode:innerNode]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, originalPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(convertedPoint)); +} + +- (void)testDisplayNodePointConversionFailureFromDisjointHierarchies +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *childNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *otherNode = [[ASDisplayNode alloc] init]; + [node addSubnode:childNode]; + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero fromNode:node selfNode:childNode], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:node selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:childNode selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero fromNode:childNode selfNode:node], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:otherNode selfNode:node], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero fromNode:otherNode selfNode:childNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero toNode:node selfNode:childNode], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:node selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:childNode selfNode:otherNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + XCTAssertNoThrow([self checkConvertPoint:CGPointZero toNode:childNode selfNode:node], @"Assertion should have succeeded; nodes are in the same hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:otherNode selfNode:node], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + XCTAssertThrows([self checkConvertPoint:CGPointZero toNode:otherNode selfNode:childNode], @"Assertion should have failed for nodes that are not in the same node hierarchy"); + + [node release]; + [childNode release]; + [otherNode release]; +} + +- (void)testDisplayNodePointConversionOnDeepHierarchies +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + // 7 deep (six below root); each one positioned at position = (1, 1) + _addTonsOfSubnodes(node, 2, 6, ^(ASDisplayNode *createdNode) { + createdNode.position = CGPointMake(1, 1); + }); + + ASDisplayNode *deepSubNode = [self _getDeepSubnodeForRoot:node withIndices:@[@1, @1, @1, @1, @1, @1]]; + + CGPoint originalPoint = CGPointMake(55, 55); + CGPoint correctPoint = CGPointMake(61, 61); + CGPoint convertedPoint = [deepSubNode convertPoint:originalPoint toNode:node]; + XCTAssertTrue(CGPointEqualToPoint(convertedPoint, correctPoint), @"Unexpected point conversion result. Point: %@ Expected conversion: %@ Actual conversion: %@", NSStringFromCGPoint(originalPoint), NSStringFromCGPoint(correctPoint), NSStringFromCGPoint(convertedPoint)); +} + +// Adds nodes (breadth-first rather than depth-first addition) +static void _addTonsOfSubnodes(ASDisplayNode *parent, NSUInteger fanout, NSUInteger depth, void (^onCreate)(ASDisplayNode *createdNode)) { + if (depth == 0) { + return; + } + + for (NSUInteger i = 0; i < fanout; i++) { + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + [parent addSubnode:subnode]; + onCreate(subnode); + [subnode release]; + } + for (NSUInteger i = 0; i < fanout; i++) { + _addTonsOfSubnodes(parent.subnodes[i], fanout, depth - 1, onCreate); + } +} + +// Convenience function for getting a node deep within a node hierarchy +- (ASDisplayNode *)_getDeepSubnodeForRoot:(ASDisplayNode *)root withIndices:(NSArray *)indexArray { + if ([indexArray count] == 0) { + return root; + } + + NSArray *subnodes = root.subnodes; + if ([subnodes count] == 0) { + XCTFail(@"Node hierarchy isn't deep enough for given index array"); + } + + NSUInteger index = [indexArray[0] unsignedIntegerValue]; + NSArray *otherIndices = [indexArray subarrayWithRange:NSMakeRange(1, [indexArray count] -1)]; + + return [self _getDeepSubnodeForRoot:subnodes[index] withIndices:otherIndices]; +} + +static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point2, CGFloat epsilon) { + CGFloat absEpsilon = fabsf(epsilon); + BOOL xOK = fabsf(point1.x - point2.x) < absEpsilon; + BOOL yOK = fabsf(point1.y - point2.y) < absEpsilon; + return xOK && yOK; +} + +- (CGPoint)checkConvertPoint:(CGPoint)point fromNode:(ASDisplayNode *)fromNode selfNode:(ASDisplayNode *)toNode +{ + CGPoint nodeConversion = [toNode convertPoint:point fromNode:fromNode]; + + UIView *fromView = fromNode.view; + UIView *toView = toNode.view; + CGPoint viewConversion = [toView convertPoint:point fromView:fromView]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(nodeConversion, viewConversion, 0.001), @"Conversion mismatch: node: %@ view: %@", NSStringFromCGPoint(nodeConversion), NSStringFromCGPoint(viewConversion)); + return nodeConversion; +} + +- (CGPoint)checkConvertPoint:(CGPoint)point toNode:(ASDisplayNode *)toNode selfNode:(ASDisplayNode *)fromNode +{ + CGPoint nodeConversion = [fromNode convertPoint:point toNode:toNode]; + + UIView *fromView = fromNode.view; + UIView *toView = toNode.view; + CGPoint viewConversion = [fromView convertPoint:point toView:toView]; + XCTAssertTrue(_CGPointEqualToPointWithEpsilon(nodeConversion, viewConversion, 0.001), @"Conversion mismatch: node: %@ view: %@", NSStringFromCGPoint(nodeConversion), NSStringFromCGPoint(viewConversion)); + return nodeConversion; +} + +- (void)executeOffThread:(void (^)(void))block +{ + __block BOOL blockExecuted = NO; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + dispatch_async(queue, ^{ + block(); + blockExecuted = YES; + dispatch_semaphore_signal(sema); + }); + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + dispatch_release(sema); + XCTAssertTrue(blockExecuted, @"Block did not finish executing. Timeout or exception?"); +} + +- (void)testReferenceCounting +{ + __block BOOL didDealloc = NO; + + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + node.willDeallocBlock = ^(ASDisplayNode *n){ + didDealloc = YES; + }; + + // verify initial + XCTAssertTrue(1 == node.retainCount, @"unexpected retain count:%d", node.retainCount); + + // verify increment + [node retain]; + XCTAssertTrue(2 == node.retainCount, @"unexpected retain count:%d", node.retainCount); + + // verify dealloc + [node release]; + [node release]; + XCTAssertTrue(didDealloc, @"unexpected node lifetime:%@", node); +} + +- (void)testAddingNodeToHierarchyRetainsNode +{ + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + + __block BOOL didDealloc = NO; + node.willDeallocBlock = ^(ASDisplayNode *n){ + didDealloc = YES; + }; + + // verify initial + XCTAssertTrue(1 == node.retainCount, @"unexpected retain count:%d", node.retainCount); + + UIView *v = [[UIView alloc] initWithFrame:CGRectZero]; + [v addSubview:node.view]; + + XCTAssertTrue(2 == node.retainCount, @"view should retain node when added. retain count:%d", node.retainCount); + + [node release]; + XCTAssertTrue(1 == node.retainCount, @"unexpected retain count:%d", node.retainCount); + + [node.view removeFromSuperview]; + XCTAssertTrue(didDealloc, @"unexpected node lifetime:%@", node); + [v release]; +} + +- (void)testMainThreadDealloc +{ + __block BOOL didDealloc = NO; + + [self executeOffThread:^{ + @autoreleasepool { + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + node.willDeallocBlock = ^(ASDisplayNode *n){ + XCTAssertTrue([NSThread isMainThread], @"unexpected node dealloc %@ %@", n, [NSThread currentThread]); + didDealloc = YES; + }; + [node release]; + } + }]; + + // deallocation should be queued on the main runloop; give it a chance + ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return didDealloc; }); + XCTAssertTrue(didDealloc, @"unexpected node lifetime"); +} + +- (void)testSubnodes +{ + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + XCTAssertNoThrow([parent addSubnode:nil], @"Don't try to add nil, but we'll deal."); + XCTAssertNoThrow([parent addSubnode:parent], @"Not good, test that we recover"); + XCTAssertEqual(0u, parent.subnodes.count, @"We shouldn't have any subnodes"); +} + +- (void)testReplaceSubnodeNoView +{ + [self checkReplaceSubnodeWithView:NO layerBacked:NO]; +} + +- (void)testReplaceSubnodeNoLayer +{ + [self checkReplaceSubnodeWithView:NO layerBacked:YES]; +} + +- (void)testReplaceSubnodeView +{ + [self checkReplaceSubnodeWithView:YES layerBacked:NO]; +} + +- (void)testReplaceSubnodeLayer +{ + [self checkReplaceSubnodeWithView:YES layerBacked:YES]; +} + + +- (void)checkReplaceSubnodeWithView:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *n in @[parent, a, b, c]) { + n.isLayerBacked = isLayerBacked; + } + + [parent addSubnode:a]; + [parent addSubnode:b]; + [parent addSubnode:c]; + + if (loaded) { + [parent layer]; + } + + DeclareNodeNamed(d); + if (loaded) { + XCTAssertFalse(d.isViewLoaded, @"Should not yet be loaded"); + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // Check initial state + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check replace 0th + [parent replaceSubnode:a withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"d,b,c", @"after replace 0th"); + XCTAssertNodesHaveParent(parent, d, b, c); + XCTAssertNodesHaveParent(nilParent, a); + if (loaded) { + XCTAssertNodesLoaded(d); + } + + [parent replaceSubnode:d withSubnode:a]; + + // Check replace 1st + [parent replaceSubnode:b withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,d,c", @"Replace"); + XCTAssertNodesHaveParent(parent, a, c, d); + XCTAssertNodesHaveParent(nilParent, b); + + [parent replaceSubnode:d withSubnode:b]; + + // Check replace 2nd + [parent replaceSubnode:c withSubnode:d]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,d", @"Replace"); + XCTAssertNodesHaveParent(parent, a, b, d); + XCTAssertNodesHaveParent(nilParent, c); + + [parent replaceSubnode:d withSubnode:c]; + + //Check initial again + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"check should back to initial"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check replace 0th with 2nd + [parent replaceSubnode:a withSubnode:c]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b", @"After replace 0th"); + XCTAssertNodesHaveParent(parent, c, b); + XCTAssertNodesHaveParent(nilParent, a,d); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; + [d release]; +} + +- (void)testAddSubnodeAsync +{ + + ASTestDisplayNode *parent = [[ASTestDisplayNode alloc] init]; + + __block BOOL calculateSizeCalled = NO; + __block CGSize passedInSize; + __block BOOL sizeCalculatedOnMainThread; + ASTestDisplayNode *nodeToAddAsync = [[ASTestDisplayNode alloc] init]; + nodeToAddAsync.bounds = CGRectMake(100, 40, 42, 56); + nodeToAddAsync.calculateSizeBlock = ^(ASDisplayNode *n, CGSize size){ + calculateSizeCalled = YES; + passedInSize = size; + sizeCalculatedOnMainThread = [NSThread isMainThread]; + return CGSizeZero; + }; + + dispatch_suspend([ASDisplayNode asyncSizingQueue]); + + __block BOOL completed = NO; + ASDisplayNode *placeholder = [parent addSubnodeAsynchronously:nodeToAddAsync completion:^(ASDisplayNode *replacement) { + completed = YES; + }]; + + // Check it hasn't been added yet + XCTAssertTrue(placeholder.supernode == parent, @"didn't make a placeholder"); + XCTAssertTrue(nodeToAddAsync.supernode == nil, @"oops, added node too soon"); + XCTAssertFalse(calculateSizeCalled, @"too soon to calculate size!"); + + dispatch_resume([ASDisplayNode asyncSizingQueue]); + + // Let the block execute on the bg thread + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return completed; }), @"Didn't finish the test fast enough"); + + XCTAssertTrue(placeholder.supernode == nil, @"didn't remove the placeholder"); + XCTAssertTrue(nodeToAddAsync.supernode == parent, @"oops, didn't add the node"); + XCTAssertTrue(calculateSizeCalled, @"too soon to calculate size!"); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(42, 56), passedInSize), @"Should pass in initial bounds size"); + XCTAssertFalse(sizeCalculatedOnMainThread, @"Should pass in initial bounds size"); + + [parent release]; + [nodeToAddAsync release]; +} + +- (void)testReplaceSubnodeAsync +{ + ASTestDisplayNode *parent = [[ASTestDisplayNode alloc] init]; + + __block BOOL calculateSizeCalled = NO; + __block CGSize passedInSize; + __block BOOL sizeCalculatedOnMainThread; + ASTestDisplayNode *nodeToAddAsync = [[ASTestDisplayNode alloc] init]; + nodeToAddAsync.bounds = CGRectMake(100, 40, 42, 56); + nodeToAddAsync.calculateSizeBlock = ^(ASDisplayNode *n, CGSize size){ + calculateSizeCalled = YES; + passedInSize = size; + sizeCalculatedOnMainThread = [NSThread isMainThread]; + return CGSizeZero; + }; + + ASDisplayNode *replaceMe = [[ASDisplayNode alloc] init]; + [parent addSubnode:replaceMe]; + + dispatch_suspend([ASDisplayNode asyncSizingQueue]); + + __block BOOL completed = NO; + + [parent replaceSubnodeAsynchronously:replaceMe withNode:nodeToAddAsync completion:^(BOOL cancelled, ASDisplayNode *nodeToAddAsyncBlockArgument, ASDisplayNode *replaceMeBlockArgument) { + XCTAssertTrue(nodeToAddAsyncBlockArgument == nodeToAddAsync, @"Passed in wrong node for the replacing node"); + XCTAssertTrue(replaceMeBlockArgument == replaceMe, @"Passed in wrong node for the replaced node"); + completed = YES; + }]; + + // Check it hasn't been replaced yet + XCTAssertTrue(replaceMe.supernode == parent, @"removed too soon!"); + XCTAssertTrue(nodeToAddAsync.supernode == nil, @"oops, added node too soon"); + XCTAssertFalse(calculateSizeCalled, @"too soon to calculate size!"); + + dispatch_resume([ASDisplayNode asyncSizingQueue]); + + // Let the block execute on the bg thread + XCTAssertTrue(ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return completed; }), @"Didn't finish the test fast enough"); + + XCTAssertTrue(replaceMe.supernode == nil, @"didn't remove old node"); + XCTAssertTrue(nodeToAddAsync.supernode == parent, @"oops, added node too soon"); + XCTAssertTrue(calculateSizeCalled, @"too soon to calculate size!"); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(42, 56), passedInSize), @"Should pass in initial bounds size"); + XCTAssertFalse(sizeCalculatedOnMainThread, @"Should pass in initial bounds size"); + + [parent release]; + [nodeToAddAsync release]; +} + +- (void)testCancelReplaceSubnodeAsyncByReplacingAgain +{ + + ASTestDisplayNode *parent = [[ASTestDisplayNode alloc] init]; + + dispatch_semaphore_t allowFirst = dispatch_semaphore_create(0); + ASTestDisplayNode *first = [[ASTestDisplayNode alloc] init]; + first.bounds = CGRectMake(100, 40, 42, 56); + first.calculateSizeBlock = ^(ASDisplayNode *n, CGSize size){ + dispatch_semaphore_wait(allowFirst, DISPATCH_TIME_FOREVER); + return CGSizeZero; + }; + + dispatch_semaphore_t allowSecond = dispatch_semaphore_create(0); + ASTestDisplayNode *second = [[ASTestDisplayNode alloc] init]; + second.bounds = CGRectMake(100, 40, 42, 56); + second.calculateSizeBlock = ^(ASDisplayNode *n, CGSize size){ + dispatch_semaphore_wait(allowSecond, DISPATCH_TIME_FOREVER); + return CGSizeMake(10, 20); + }; + + + ASDisplayNode *replaceMe = [[ASDisplayNode alloc] init]; + [parent addSubnode:replaceMe]; + + __block BOOL firstFinished = NO; + [parent replaceSubnodeAsynchronously:replaceMe withNode:first completion:^(BOOL cancelled, ASDisplayNode *firstBlockArgument, ASDisplayNode *replaceMeBlockArgument) { + XCTAssertNil(firstBlockArgument, @"Should have cancelled"); + XCTAssertNil(replaceMeBlockArgument, @"Should have cancelled"); + firstFinished = YES; + }]; + + __block BOOL secondFinished = NO; + [parent replaceSubnodeAsynchronously:replaceMe withNode:second completion:^(BOOL cancelled, ASDisplayNode *secondBlockArgument, ASDisplayNode *replaceMeBlockArgument) { + XCTAssertNotNil(secondBlockArgument, @"Should have the relevant node passed in"); + XCTAssertNotNil(replaceMeBlockArgument, @"Should have the relevant node passed in"); + secondFinished = YES; + }]; + + XCTAssertTrue(replaceMe.supernode == parent, @"didn't remove old node"); + XCTAssertTrue(first.supernode == nil, @"oops, added node too soon"); + XCTAssertTrue(second.supernode == nil, @"oops, added node too soon"); + XCTAssertFalse(first.isViewLoaded, @"first view loaded too soon"); + XCTAssertFalse(second.isViewLoaded, @"second view loaded too soon"); + + // Allow first to complete, but verify that the nodes are nil, indicating cancellation (asserts are in blocks above) + dispatch_semaphore_signal(allowFirst); + + // Let the work execute on the bg thread + ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return firstFinished; }); + + XCTAssertTrue(first.supernode == nil, @"first should not be added to the hierarchy ever"); + XCTAssertTrue(second.supernode == nil, @"second should not yet be added to the hierarchy"); + + dispatch_semaphore_signal(allowSecond); + ASDisplayNodeRunRunLoopUntilBlockIsTrue(^BOOL{ return secondFinished; }); + + XCTAssertTrue(first.supernode == nil, @"first should not be added to the hierarchy ever"); + XCTAssertTrue(second.supernode == parent, @"second should be added now, woot!"); + XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(10, 20), [second calculatedSize]), @"Should have correct calculatedSize"); + + [parent release]; + [first release]; + [second release]; + dispatch_release(allowFirst); + dispatch_release(allowSecond); +} + +- (void)testInsertSubnodeAtIndexView +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeAtIndexLayer +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:YES layerBacked:YES]; +} + +- (void)testInsertSubnodeAtIndexNoView +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeAtIndexNoLayer +{ + [self checkInsertSubnodeAtIndexWithViewLoaded:NO layerBacked:YES]; +} + +- (void)checkInsertSubnodeAtIndexWithViewLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *v in @[parent, a, b, c]) { + v.isLayerBacked = isLayerBacked; + } + + // Load parent + if (loaded) { + (void)[parent layer]; + } + + // Add another subnode to test creation after parent is loaded + DeclareNodeNamed(d); + d.isLayerBacked = isLayerBacked; + if (loaded) { + XCTAssertFalse(d.isViewLoaded, @"Should not yet be loaded"); + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // Check initial state + XCTAssertEqual(0u, parent.subnodes.count, @"Should have the right subnode count"); + + // Check insert at 0th () => (a,b,c) + [parent insertSubnode:c atIndex:0]; + [parent insertSubnode:b atIndex:0]; + [parent insertSubnode:a atIndex:0]; + + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + if (loaded) { + XCTAssertNodesLoaded(a, b, c); + } else { + XCTAssertNodesNotLoaded(a, b, c); + } + + // Check insert at 1st (a,b,c) => (a,d,b,c) + [parent insertSubnode:d atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,d,b,c", @"initial state"); + XCTAssertNodesHaveParent(parent, a, b, c, d); + if (loaded) { + XCTAssertNodesLoaded(d); + } + + // Reset + [d removeFromSupernode]; + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Bad removal of d"); + XCTAssertNodesHaveParent(nilParent, d); + + // Check insert at last position + [parent insertSubnode:d atIndex:3]; + + XCTAssertEqual(4u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c,d", @"insert at last position."); + XCTAssertNodesHaveParent(parent, a, b, c, d); + + // Reset + [d removeFromSupernode]; + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqualObjects(nilParent, d.supernode, @"d's parent is messed up"); + + + // Check insert at invalid index + XCTAssertThrows([parent insertSubnode:d atIndex:NSNotFound], @"Should not allow insertion at invalid index"); + XCTAssertThrows([parent insertSubnode:d atIndex:-1], @"Should not allow insertion at invalid index"); + + // Should have same state as before + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Check reordering existing subnodes with the insert API + // Move c to front + [parent insertSubnode:c atIndex:0]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Move to front when already a subnode"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Move c to middle + [parent insertSubnode:c atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Move c to middle"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Insert c at the index it's already at + [parent insertSubnode:c atIndex:1]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + // Insert c at 0th when it's already in the array + [parent insertSubnode:c atIndex:2]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b,c", @"Funny business should not corrupt state"); + XCTAssertNodesHaveParent(parent, a, b, c); + XCTAssertNodesHaveParent(nilParent, d); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; + [d release]; +} + +// This tests our resiliancy to having other views and layers inserted into our view or layer +- (void)testInsertSubviewAtIndexWithMeddlingViewsAndLayersViewBacked +{ + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareViewNamed(d); + DeclareLayerNamed(e); + + [parent layer]; + + // (a,b) + [parent addSubnode:a]; + [parent addSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,b", @"Didn't match"); + + // (a,b) => (a,d,b) + [parent.view insertSubview:d aboveSubview:a.view]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,b", @"Didn't match"); + + // (a,d,b) => (a,e,d,b) + [parent.layer insertSublayer:e above:a.layer]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,e,d,b", @"Didn't match"); + + // (a,e,d,b) => (a,e,d,c,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,e,d,c,b", @"Didn't match"); + + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqual(4u, parent.view.subviews.count, @"Should have the right subview count"); + XCTAssertEqual(5u, parent.layer.sublayers.count, @"Should have the right sublayer count"); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; + [d release]; +} + +- (void)testAppleBugInsertSubview +{ + DeclareViewNamed(parent); + + DeclareLayerNamed(aa); + DeclareLayerNamed(ab); + DeclareViewNamed(a); + DeclareLayerNamed(ba); + DeclareLayerNamed(bb); + DeclareLayerNamed(bc); + DeclareLayerNamed(bd); + DeclareViewNamed(c); + DeclareViewNamed(d); + DeclareLayerNamed(ea); + DeclareLayerNamed(eb); + DeclareLayerNamed(ec); + + [parent.layer addSublayer:aa]; + [parent.layer addSublayer:ab]; + [parent addSubview:a]; + [parent.layer addSublayer:ba]; + [parent.layer addSublayer:bb]; + [parent.layer addSublayer:bc]; + [parent.layer addSublayer:bd]; + [parent addSubview:d]; + [parent.layer addSublayer:ea]; + [parent.layer addSublayer:eb]; + [parent.layer addSublayer:ec]; + + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,ab,a,ba,bb,bc,bd,d,ea,eb,ec", @"Should be in order"); + + // Should insert at SUBVIEW index 1, right?? + [parent insertSubview:c atIndex:1]; + + // You would think that this would be true, but instead it inserts it at the SUBLAYER index 1 +// XCTAssertEquals([parent.subviews indexOfObjectIdenticalTo:c], 1u, @"Should have index 1 after insert"); +// XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,ab,a,ba,bb,bc,bd,c,d,ea,eb,ec", @"Should be in order"); + + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"aa,c,ab,a,ba,bb,bc,bd,d,ea,eb,ec", @"Apple has fixed insertSubview:atIndex:. You must update insertSubnode: etc. APIS to accomidate this."); +} + +// This tests our resiliancy to having other views and layers inserted into our view or layer +- (void)testInsertSubviewAtIndexWithMeddlingView +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareViewNamed(d); + DeclareLayerNamed(e); + + [parent layer]; + + // (a,b) + [parent addSubnode:a]; + [parent addSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,b", @"Didn't match"); + + // (a,b) => (a,d,b) + [parent.view insertSubview:d aboveSubview:a.view]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,b", @"Didn't match"); + + // (a,e,d,b) => (a,d,>c<,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertEqualObjects(orderStringFromSublayers(parent.layer), @"a,d,c,b", @"Didn't match"); + + XCTAssertEqual(3u, parent.subnodes.count, @"Should have the right subnode count"); + XCTAssertEqual(4u, parent.view.subviews.count, @"Should have the right subview count"); + XCTAssertEqual(4u, parent.layer.sublayers.count, @"Should have the right sublayer count"); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; + [d release]; +} + + +- (void)testInsertSubnodeBelowWithView +{ + [self checkInsertSubnodeBelowWithView:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeBelowWithNoView +{ + [self checkInsertSubnodeBelowWithView:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeBelowWithNoLayer +{ + [self checkInsertSubnodeBelowWithView:NO layerBacked:YES]; +} + +- (void)testInsertSubnodeBelowWithLayer +{ + [self checkInsertSubnodeBelowWithView:YES layerBacked:YES]; +} + + +- (void)checkInsertSubnodeBelowWithView:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *v in @[parent, a, b, c]) { + v.isLayerBacked = isLayerBacked; + } + + [parent addSubnode:b]; + + if (loaded) { + [parent layer]; + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // (b) => (a, b) + [parent insertSubnode:a belowSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, b); + XCTAssertNodesHaveParent(nilParent, c); + + // (a,b) => (c,a,b) + [parent insertSubnode:c belowSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, b, c); + + // Check insertSubnode with no below + XCTAssertThrows([parent insertSubnode:b belowSubnode:nil], @"Can't insert below a nil"); + // Check nothing was inserted + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,a,b", @"Incorrect insertion below"); + + + XCTAssertThrows([parent insertSubnode:nil belowSubnode:nil], @"Can't insert a nil subnode"); + XCTAssertThrows([parent insertSubnode:nil belowSubnode:a], @"Can't insert a nil subnode"); + + // Check inserting below when you're already in the array + // (c,a,b) => (a,c,b) + [parent insertSubnode:c belowSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, c, b); + + // Check what happens when you try to insert a node below itself (should do nothing) + // (a,c,b) => (a,c,b) + [parent insertSubnode:c belowSubnode:c]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Incorrect insertion below"); + XCTAssertNodesHaveParent(parent, a, c, b); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; +} + +- (void)testInsertSubnodeAboveWithView +{ + [self checkInsertSubnodeAboveLoaded:YES layerBacked:NO]; +} + +- (void)testInsertSubnodeAboveWithNoView +{ + [self checkInsertSubnodeAboveLoaded:NO layerBacked:NO]; +} + +- (void)testInsertSubnodeAboveWithLayer +{ + [self checkInsertSubnodeAboveLoaded:YES layerBacked:YES]; +} + +- (void)testInsertSubnodeAboveWithNoLayer +{ + [self checkInsertSubnodeAboveLoaded:NO layerBacked:YES]; +} + + +- (void)checkInsertSubnodeAboveLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + DeclareNodeNamed(parent); + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + + for (ASDisplayNode *n in @[parent, a, b, c]) { + n.isLayerBacked = isLayerBacked; + } + + [parent addSubnode:a]; + + if (loaded) { + [parent layer]; + } + + // Shut the type mismatch up + ASDisplayNode *nilParent = nil; + + // (a) => (a,b) + [parent insertSubnode:b aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,b", @"Insert subnode above"); + XCTAssertNodesHaveParent(parent, a,b); + XCTAssertNodesHaveParent(nilParent, c); + + // (a,b) => (a,c,b) + [parent insertSubnode:c aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"After insert c above a"); + + // Check insertSubnode with invalid parameters throws and doesn't change anything + // (a,c,b) => (a,c,b) + XCTAssertThrows([parent insertSubnode:b aboveSubnode:nil], @"Can't insert below a nil"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + XCTAssertThrows([parent insertSubnode:nil aboveSubnode:nil], @"Can't insert a nil subnode"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + XCTAssertThrows([parent insertSubnode:nil aboveSubnode:a], @"Can't insert a nil subnode"); + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"a,c,b", @"Check no monkey business"); + + // Check inserting above when you're already in the array + // (a,c,b) => (c,b,a) + [parent insertSubnode:a aboveSubnode:b]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b,a", @"Check inserting above when you're already in the array"); + XCTAssertNodesHaveParent(parent, a, c, b); + + // Check what happens when you try to insert a node above itself (should do nothing) + // (c,b,a) => (c,b,a) + [parent insertSubnode:a aboveSubnode:a]; + XCTAssertNodeSubnodeSubviewSublayerOrder(parent, loaded, isLayerBacked, @"c,b,a", @"Insert above self should not change anything"); + XCTAssertNodesHaveParent(parent, a, c, b); + + //TODO: assert that things deallocate immediately and don't have latent autoreleases in here + [parent release]; + [a release]; + [b release]; + [c release]; +} + +- (void)checkBackgroundColorOpaqueRelationshipWithViewLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.isLayerBacked = isLayerBacked; + + if (loaded) { + // Force load + [node layer]; + } + + XCTAssertTrue(node.opaque, @"Node should start opaque"); + XCTAssertTrue(node.layer.opaque, @"Node should start opaque"); + + node.backgroundColor = [UIColor clearColor]; + + // This could be debated, but at the moment we differ from UIView's behavior to change the other property in response + XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque"); + XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque"); + + [node layer]; + + XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque"); + XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque"); + + [node release]; +} + +- (void)testBackgroundColorOpaqueRelationshipView +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:YES layerBacked:NO]; +} + +- (void)testBackgroundColorOpaqueRelationshipLayer +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:YES layerBacked:YES]; +} + +- (void)testBackgroundColorOpaqueRelationshipNoView +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:NO layerBacked:NO]; +} + +- (void)testBackgroundColorOpaqueRelationshipNoLayer +{ + [self checkBackgroundColorOpaqueRelationshipWithViewLoaded:NO layerBacked:YES]; +} + +- (void)testInitWithViewClass +{ + ASDisplayNode *scrollNode = [[ASDisplayNode alloc] initWithViewClass:[UIScrollView class]]; + + XCTAssertFalse(scrollNode.isLayerBacked, @"Can't be layer backed"); + XCTAssertFalse(scrollNode.isViewLoaded, @"Shouldn't have a view yet"); + + scrollNode.frame = CGRectMake(12, 52, 100, 53); + scrollNode.alpha = 0.5; + + XCTAssertTrue([scrollNode.view isKindOfClass:[UIScrollView class]], @"scrollview should load as expected"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(12, 52, 100, 53), scrollNode.frame), @"Should have set the frame on the scroll node"); + XCTAssertEqual(0.5f, scrollNode.alpha, @"Alpha not working"); +} + +- (void)testInitWithLayerClass +{ + ASDisplayNode *transformNode = [[ASDisplayNode alloc] initWithLayerClass:[CATransformLayer class]]; + + XCTAssertTrue(transformNode.isLayerBacked, @"Created with layer class => should be layer-backed by default"); + XCTAssertFalse(transformNode.isViewLoaded, @"Shouldn't have a view yet"); + + transformNode.frame = CGRectMake(12, 52, 100, 53); + transformNode.alpha = 0.5; + + XCTAssertTrue([transformNode.layer isKindOfClass:[CATransformLayer class]], @"scrollview should load as expected"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(12, 52, 100, 53), transformNode.frame), @"Should have set the frame on the scroll node"); + XCTAssertEqual(0.5f, transformNode.alpha, @"Alpha not working"); +} + +static bool stringContainsPointer(NSString *description, const void *p) { + return [description rangeOfString:[NSString stringWithFormat:@"%p", p]].location != NSNotFound; +} + +- (void)testDebugDescription +{ + // View node has subnodes. Make sure all of the nodes are included in the description + ASDisplayNode *parent = [[ASDisplayNode alloc] init]; + + ASDisplayNode *a = [[[ASDisplayNode alloc] init] autorelease]; + a.isLayerBacked = YES; + ASDisplayNode *b = [[[ASDisplayNode alloc] init] autorelease]; + b.isLayerBacked = YES; + b.frame = CGRectMake(0, 0, 100, 123); + ASDisplayNode *c = [[[ASDisplayNode alloc] init] autorelease]; + + for (ASDisplayNode *child in @[a, b, c]) { + [parent addSubnode:child]; + } + + NSString *nodeDescription = [parent displayNodeRecursiveDescription]; + + // Make sure [parent recursiveDescription] contains a, b, and c's pointer string + XCTAssertTrue(stringContainsPointer(nodeDescription, a), @"Layer backed node not present in [parent displayNodeRecursiveDescription]"); + XCTAssertTrue(stringContainsPointer(nodeDescription, b), @"Layer-backed node not present in [parent displayNodeRecursiveDescription]"); + XCTAssertTrue(stringContainsPointer(nodeDescription, c), @"View-backed node not present in [parent displayNodeRecursiveDescription]"); + + NSString *viewDescription = [parent.view valueForKey:@"recursiveDescription"]; + + // Make sure string contains a, b, and c's pointer string + XCTAssertTrue(stringContainsPointer(viewDescription, a), @"Layer backed node not present"); + XCTAssertTrue(stringContainsPointer(viewDescription, b), @"Layer-backed node not present"); + XCTAssertTrue(stringContainsPointer(viewDescription, c), @"View-backed node not present"); + + // Make sure layer names have display node in description + XCTAssertTrue(stringContainsPointer([a.layer debugDescription], a), @"Layer backed node not present"); + XCTAssertTrue(stringContainsPointer([b.layer debugDescription], b), @"Layer-backed node not present"); + + [parent release]; +} + +- (void)checkNameInDescriptionIsLayerBacked:(BOOL)isLayerBacked +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.isLayerBacked = isLayerBacked; + + XCTAssertFalse([node.description rangeOfString:@"name"].location != NSNotFound, @"Shouldn't reference 'name' in description"); + node.name = @"big troll eater name"; + + XCTAssertTrue([node.description rangeOfString:node.name].location != NSNotFound, @"Name didn't end up in description"); + XCTAssertTrue([node.description rangeOfString:@"name"].location != NSNotFound, @"Shouldn't reference 'name' in description"); + [node layer]; + XCTAssertTrue([node.description rangeOfString:node.name].location != NSNotFound, @"Name didn't end up in description"); + XCTAssertTrue([node.description rangeOfString:@"name"].location != NSNotFound, @"Shouldn't reference 'name' in description"); + + [node release]; +} + +- (void)testNameInDescriptionLayer +{ + [self checkNameInDescriptionIsLayerBacked:YES]; +} + +- (void)testNameInDescriptionView +{ + [self checkNameInDescriptionIsLayerBacked:NO]; +} + + +@end diff --git a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h new file mode 100644 index 0000000000..9c2a1fe524 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h @@ -0,0 +1,13 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +typedef BOOL (^as_condition_block_t)(void); + +BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block); diff --git a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m new file mode 100644 index 0000000000..18fb738035 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m @@ -0,0 +1,41 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDisplayNodeTestsHelper.h" + +#import + +#import + +// Poll the condition 1000 times a second. +static CFTimeInterval kSingleRunLoopTimeout = 0.001; + +// Time out after 30 seconds. +static CFTimeInterval kTimeoutInterval = 30.0f; + +BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block) +{ + CFTimeInterval timeoutDate = CACurrentMediaTime() + kTimeoutInterval; + BOOL passed = NO; + while (true) { + OSMemoryBarrier(); + passed = block(); + OSMemoryBarrier(); + if (passed) { + break; + } + CFTimeInterval now = CACurrentMediaTime(); + if (now > timeoutDate) { + break; + } + // Run until the poll timeout or until timeoutDate, whichever is first. + CFTimeInterval runLoopTimeout = MIN(kSingleRunLoopTimeout, timeoutDate - now); + CFRunLoopRunInMode(kCFRunLoopDefaultMode, runLoopTimeout, true); + } + return passed; +} diff --git a/AsyncDisplayKitTests/ASMutableAttributedStringBuilderTests.m b/AsyncDisplayKitTests/ASMutableAttributedStringBuilderTests.m new file mode 100644 index 0000000000..0d6cfbe269 --- /dev/null +++ b/AsyncDisplayKitTests/ASMutableAttributedStringBuilderTests.m @@ -0,0 +1,76 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASMutableAttributedStringBuilder.h" + +@interface ASMutableAttributedStringBuilderTests : XCTestCase + +@end + +@implementation ASMutableAttributedStringBuilderTests + +- (NSString *)_string +{ + return @"Normcore PBR hella, viral slow-carb mustache chillwave church-key cornhole messenger bag swag vinyl biodiesel ethnic. Fashion axe messenger bag raw denim street art. Flannel Wes Anderson normcore church-key 8-bit. Master cleanse four loko try-hard Carles stumptown ennui, twee literally wayfarers kitsch tofu PBR. Cliche organic post-ironic Wes Anderson kale chips fashion axe. Narwhal Blue Bottle sustainable, Odd Future Godard sriracha banjo disrupt Marfa irony pug Wes Anderson YOLO yr church-key. Mlkshk Intelligentsia semiotics quinoa, butcher meggings wolf Bushwick keffiyeh ethnic pour-over Pinterest letterpress."; +} + +- (ASMutableAttributedStringBuilder *)_builder +{ + return [[ASMutableAttributedStringBuilder alloc] initWithString:[self _string]]; +} + +- (NSRange)_randomizedRangeForStringBuilder:(ASMutableAttributedStringBuilder *)builder +{ + NSUInteger loc = arc4random() % (builder.length - 1); + NSUInteger len = MAX(arc4random() % (builder.length - loc), 1); + return NSMakeRange(loc, len); +} + +- (void)testSimpleAttributions +{ + // Add a attributes, and verify that they get set on the correct locations. + for (int i = 0; i < 100; i++) { + ASMutableAttributedStringBuilder *builder = [self _builder]; + NSRange range = [self _randomizedRangeForStringBuilder:builder]; + NSString *keyValue = [NSString stringWithFormat:@"%d", i]; + [builder addAttribute:keyValue value:keyValue range:range]; + NSAttributedString *attrStr = [builder composedAttributedString]; + XCTAssertEqual(builder.length, attrStr.length, @"out string should have same length as builder"); + __block BOOL found = NO; + [attrStr enumerateAttributesInRange:NSMakeRange(0, attrStr.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange r, BOOL *stop) { + if ([attrs[keyValue] isEqualToString:keyValue]) { + XCTAssertTrue(NSEqualRanges(range, r), @"enumerated range %@ should be equal to the set range %@", NSStringFromRange(r), NSStringFromRange(range)); + found = YES; + } + }]; + XCTAssertTrue(found, @"enumeration should have found the attribute we set"); + } +} + +- (void)testSetOverAdd +{ + ASMutableAttributedStringBuilder *builder = [self _builder]; + NSRange addRange = NSMakeRange(0, builder.length); + NSRange setRange = NSMakeRange(0, 1); + [builder addAttribute:@"attr" value:@"val1" range:addRange]; + [builder setAttributes:@{@"attr" : @"val2"} range:setRange]; + NSAttributedString *attrStr = [builder composedAttributedString]; + NSRange setRangeOut; + NSString *setAttr = [attrStr attribute:@"attr" atIndex:0 effectiveRange:&setRangeOut]; + XCTAssertTrue(NSEqualRanges(setRange, setRangeOut), @"The out set range should equal the range we used originally"); + XCTAssertEqualObjects(setAttr, @"val2", @"the set value should be val2"); + + NSRange addRangeOut; + NSString *addAttr = [attrStr attribute:@"attr" atIndex:2 effectiveRange:&addRangeOut]; + XCTAssertTrue(NSEqualRanges(NSMakeRange(1, builder.length - 1), addRangeOut), @"the add range should only cover beyond the set range"); + XCTAssertEqualObjects(addAttr, @"val1", @"the added attribute should be present at index 2"); +} + +@end diff --git a/AsyncDisplayKitTests/ASTextNodeCoreTextAdditionsTests.m b/AsyncDisplayKitTests/ASTextNodeCoreTextAdditionsTests.m new file mode 100644 index 0000000000..60a80e4556 --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeCoreTextAdditionsTests.m @@ -0,0 +1,48 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "ASTextNodeCoreTextAdditions.h" + +@interface ASTextNodeCoreTextAdditionsTests : XCTestCase + +@end + +@implementation ASTextNodeCoreTextAdditionsTests + +- (void)testAttributeCleansing +{ + NSMutableAttributedString *testString = [[NSMutableAttributedString alloc] initWithString:@"Test" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12.0]}]; + CFRange cfRange = CFRangeMake(0, testString.length); + CGColorRef blueColor = CGColorRetain([UIColor blueColor].CGColor); + CFAttributedStringSetAttribute((CFMutableAttributedStringRef)testString, + cfRange, + kCTForegroundColorAttributeName, + blueColor); + NSAttributedString *expectedCleansedString = [[NSAttributedString alloc] initWithString:@"Test" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12.0], + NSForegroundColorAttributeName:[UIColor colorWithCGColor:blueColor]}]; + + NSAttributedString *actualCleansedString = ASCleanseAttributedStringOfCoreTextAttributes(testString); + XCTAssertTrue([expectedCleansedString isEqualToAttributedString:actualCleansedString], @"Expected the %@ core text attribute to be cleansed from the string %@", kCTForegroundColorFromContextAttributeName, actualCleansedString); + CGColorRelease(blueColor); +} + +- (void)testNoAttributeCleansing +{ + NSMutableAttributedString *testString = [[NSMutableAttributedString alloc] initWithString:@"Test" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12.0], + NSForegroundColorAttributeName : [UIColor blueColor]}]; + + NSAttributedString *actualCleansedString = ASCleanseAttributedStringOfCoreTextAttributes(testString); + XCTAssertTrue([testString isEqualToAttributedString:actualCleansedString], @"Expected the output string %@ to be the same as the input %@ if there are no core text attributes", actualCleansedString, testString); +} + + +@end diff --git a/AsyncDisplayKitTests/ASTextNodeRendererTests.m b/AsyncDisplayKitTests/ASTextNodeRendererTests.m new file mode 100644 index 0000000000..740c5bfb02 --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeRendererTests.m @@ -0,0 +1,130 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import "ASTextNodeRenderer.h" + +@interface ASTextNodeRendererTests : XCTestCase + +@property (nonatomic, readwrite, strong) ASTextNodeRenderer *renderer; +@property (nonatomic, copy, readwrite) NSAttributedString *attributedString; +@property (nonatomic, copy, readwrite) NSAttributedString *truncationString; +@property (nonatomic, readwrite, assign) NSLineBreakMode truncationMode; +@property (nonatomic, readwrite, assign) CGFloat lineSpacing; + +@property (nonatomic, readwrite, assign) CGSize constrainedSize; + +@end + +@implementation ASTextNodeRendererTests + +- (void)setUp +{ + [super setUp]; + + _truncationMode = NSLineBreakByWordWrapping; + + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + _lineSpacing = 14.0; + paragraphStyle.lineSpacing = _lineSpacing; + paragraphStyle.maximumLineHeight = _lineSpacing; + paragraphStyle.minimumLineHeight = _lineSpacing; + NSDictionary *attributes = @{ NSFontAttributeName : [UIFont systemFontOfSize:12.0], + NSParagraphStyleAttributeName : paragraphStyle }; + _attributedString = [[NSAttributedString alloc] initWithString:@"Lorem ipsum" attributes:attributes]; + _truncationString = [[NSAttributedString alloc] initWithString:@"More"]; + + _constrainedSize = CGSizeMake(FLT_MAX, FLT_MAX); +} + +- (void)setUpRenderer +{ + _renderer = [[ASTextNodeRenderer alloc] initWithAttributedString:_attributedString + truncationString:_truncationString + truncationMode:_truncationMode + constrainedSize:_constrainedSize]; + +} + +- (void)testCalculateSize +{ + [self setUpRenderer]; + + CGSize size = [_renderer size]; + XCTAssertTrue(size.width > 0, @"Should have a nonzero width"); + XCTAssertTrue(size.height > 0, @"Should have a nonzero height"); +} + +- (void)testNumberOfLines +{ + [self setUpRenderer]; + CGSize size = [_renderer size]; + NSInteger numberOfLines = size.height / _lineSpacing; + XCTAssertTrue(numberOfLines == 1 , @"If constrained height (%f) is float max, then there should only be one line of text. Size %@", _constrainedSize.width, NSStringFromCGSize(size)); +} + +- (void)testNoTruncationIfEnoughSpace +{ + [self setUpRenderer]; + [_renderer size]; + NSRange stringRange = NSMakeRange(0, _attributedString.length); + NSRange visibleRange = [_renderer visibleRange]; + XCTAssertTrue(NSEqualRanges(stringRange, visibleRange), @"There should be no truncation if the text has plenty of space to lay out"); + XCTAssertTrue(NSEqualRanges([_renderer truncationStringCharacterRange], NSMakeRange(NSNotFound, _truncationString.length)), @"There should be no range for the truncation string if no truncation is occurring"); +} + +- (void)testTruncation +{ + [self setUpRenderer]; + CGSize calculatedSize = [_renderer size]; + + // Make the constrained size just a *little* too small + _constrainedSize = CGSizeMake(calculatedSize.width - 2, calculatedSize.height); + _renderer = nil; + [self setUpRenderer]; + [_renderer size]; + NSRange stringRange = NSMakeRange(0, _attributedString.length); + NSRange visibleRange = [_renderer visibleRange]; + XCTAssertTrue(visibleRange.length < stringRange.length, @"Some truncation should occur if the constrained size is smaller than the previously calculated bounding size. String length %d, visible range %@", _attributedString.length, NSStringFromRange(visibleRange)); + NSRange truncationRange = [_renderer truncationStringCharacterRange]; + XCTAssertTrue(truncationRange.location == NSMaxRange(visibleRange), @"Truncation location (%d) should be after the end of the visible range (%d)", truncationRange.location, NSMaxRange(visibleRange)); + XCTAssertTrue(truncationRange.length == _truncationString.length, @"Truncation string length (%d) should be the full length of the supplied truncation string (%@)", truncationRange.length, _truncationString.string); +} + +/** + * We don't want to decrease the total number of lines, i.e. truncate too aggressively, + * But we also don't want to add extra lines just to display our truncation message + */ +- (void)testTruncationConservesOriginalHeight +{ + [self setUpRenderer]; + CGSize calculatedSize = [_renderer size]; + + // Make the constrained size just a *little* too small + _constrainedSize = CGSizeMake(calculatedSize.width - 1, calculatedSize.height); + [self setUpRenderer]; + CGSize calculatedSizeWithTruncation = [_renderer size]; + // Floating point equality + XCTAssertTrue(fabsf(calculatedSizeWithTruncation.height - calculatedSize.height) < .001, @"The height after truncation (%f) doesn't match the normal calculated height (%f)", calculatedSizeWithTruncation.height, calculatedSize.height); +} + +- (void)testNoCrashOnTappingEmptyTextNode +{ + _attributedString = [[NSAttributedString alloc] initWithString:@""]; + [self setUpRenderer]; + [_renderer size]; + [_renderer enumerateTextIndexesAtPosition:CGPointZero usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) { + XCTFail(@"Shouldn't be any text indexes to enumerate"); + }]; + +} + +@end diff --git a/AsyncDisplayKitTests/ASTextNodeShadowerTests.m b/AsyncDisplayKitTests/ASTextNodeShadowerTests.m new file mode 100644 index 0000000000..f260d27768 --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeShadowerTests.m @@ -0,0 +1,156 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASTextNodeShadower.h" + +@interface ASTextNodeShadowerTests : XCTestCase + +@property (nonatomic, readwrite, strong) ASTextNodeShadower *shadower; + +@end + +@implementation ASTextNodeShadowerTests + +- (void)testInstantiation +{ + CGSize shadowOffset = CGSizeMake(3, 5); + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 0.3; + CGFloat shadowRadius = 4.2; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + XCTAssertNotNil(_shadower, @"Couldn't instantiate shadow drawer"); + XCTAssertTrue(CGSizeEqualToSize(_shadower.shadowOffset, shadowOffset), @"Failed to set shadowOffset (%@) to %@", NSStringFromCGSize(_shadower.shadowOffset), NSStringFromCGSize(shadowOffset)); + XCTAssertTrue(_shadower.shadowColor == shadowColor, @"Failed to set shadowColor (%@) to %@", _shadower.shadowColor, shadowColor); + XCTAssertTrue(_shadower.shadowOpacity == shadowOpacity, @"Failed to set shadowOpacity (%f) to %f", _shadower.shadowOpacity, shadowOpacity); + XCTAssertTrue(_shadower.shadowRadius == shadowRadius, @"Failed to set shadowRadius (%f) to %f", _shadower.shadowRadius, shadowRadius); + CGColorRelease(shadowColor); +} + +- (void)testNoShadowIfNoRadiusAndNoOffset +{ + CGSize shadowOffset = CGSizeZero; + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 0.3; + CGFloat shadowRadius = 0; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, UIEdgeInsetsZero), @"There should be no shadow padding if shadow radius is zero"); + CGColorRelease(shadowColor); +} + +- (void)testShadowIfOffsetButNoRadius +{ + CGSize shadowOffset = CGSizeMake(3, 5); + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 0.3; + CGFloat shadowRadius = 0; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + UIEdgeInsets expectedInsets = UIEdgeInsetsMake(0, 0, -5, -3); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, expectedInsets), @"Expected insets %@, encountered insets %@", NSStringFromUIEdgeInsets(expectedInsets), NSStringFromUIEdgeInsets(shadowPadding)); + CGColorRelease(shadowColor); +} + +- (void)testNoShadowIfNoOpacity +{ + CGSize shadowOffset = CGSizeMake(3, 5); + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 0; + CGFloat shadowRadius = 4; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, UIEdgeInsetsZero), @"There should be no shadow padding if shadow opacity is zero"); + CGColorRelease(shadowColor); +} + +- (void)testShadowPaddingForRadiusOf4 +{ + CGSize shadowOffset = CGSizeZero; + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 1; + CGFloat shadowRadius = 4; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + UIEdgeInsets expectedInsets = UIEdgeInsetsMake(-shadowRadius, -shadowRadius, -shadowRadius, -shadowRadius); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, expectedInsets), @"Unexpected edge insets %@ for radius of %f ", NSStringFromUIEdgeInsets(shadowPadding), shadowRadius); + CGColorRelease(shadowColor); +} + +- (void)testShadowPaddingForRadiusOf4OffsetOf11 +{ + CGSize shadowOffset = CGSizeMake(1, 1); + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 1; + CGFloat shadowRadius = 4; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + UIEdgeInsets expectedInsets = UIEdgeInsetsMake(-shadowRadius + shadowOffset.height, // Top: -3 + -shadowRadius + shadowOffset.width, // Left: -3 + -shadowRadius - shadowOffset.height, // Bottom: -5 + -shadowRadius - shadowOffset.width); // Right: -5 + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, expectedInsets), @"Unexpected edge insets %@ for radius of %f ", NSStringFromUIEdgeInsets(shadowPadding), shadowRadius); + CGColorRelease(shadowColor); +} + +- (void)testShadowPaddingForRadiusOf4OffsetOfNegative11 +{ + CGSize shadowOffset = CGSizeMake(-1, -1); + CGColorRef shadowColor = CGColorRetain([UIColor blackColor].CGColor); + CGFloat shadowOpacity = 1; + CGFloat shadowRadius = 4; + _shadower = [[ASTextNodeShadower alloc] initWithShadowOffset:shadowOffset + shadowColor:shadowColor + shadowOpacity:shadowOpacity + shadowRadius:shadowRadius]; + UIEdgeInsets shadowPadding = [_shadower shadowPadding]; + UIEdgeInsets expectedInsets = UIEdgeInsetsMake(-shadowRadius + shadowOffset.height, // Top: -3 + -shadowRadius + shadowOffset.width, // Left: -5 + -shadowRadius - shadowOffset.height, // Bottom: -5 + -shadowRadius - shadowOffset.width); // Right: -3 + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(shadowPadding, expectedInsets), @"Unexpected edge insets %@ for radius of %f ", NSStringFromUIEdgeInsets(shadowPadding), shadowRadius); + CGColorRelease(shadowColor); +} + +- (void)testASDNEdgeInsetsInvert +{ + UIEdgeInsets insets = UIEdgeInsetsMake(-5, -7, -3, -2); + UIEdgeInsets invertedInsets = ASDNEdgeInsetsInvert(insets); + UIEdgeInsets expectedInsets = UIEdgeInsetsMake(5, 7, 3, 2); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(invertedInsets, expectedInsets), @"Expected %@, actual result %@", NSStringFromUIEdgeInsets(expectedInsets), NSStringFromUIEdgeInsets(invertedInsets)); +} + +- (void)testASDNEdgeInsetsInvertDoubleNegation +{ + CGRect originalRect = CGRectMake(31, 32, 33, 34); + UIEdgeInsets insets = UIEdgeInsetsMake(-5, -7, -3, -2); + CGRect insettedRect = UIEdgeInsetsInsetRect(originalRect, insets); + CGRect outsettedInsettedRect = UIEdgeInsetsInsetRect(insettedRect, ASDNEdgeInsetsInvert(insets)); + XCTAssertTrue(CGRectEqualToRect(originalRect, outsettedInsettedRect), @"Insetting a CGRect, and then outsetting it (insetting with the negated edge insets) should return the original CGRect"); +} + +@end diff --git a/AsyncDisplayKitTests/ASTextNodeTests.m b/AsyncDisplayKitTests/ASTextNodeTests.m new file mode 100644 index 0000000000..f0a73c802f --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeTests.m @@ -0,0 +1,180 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +#import + +#import + +@interface ASTextNodeTestDelegate : NSObject + +@property (nonatomic, copy, readonly) NSString *tappedLinkAttribute; +@property (nonatomic, assign, readonly) id tappedLinkValue; + + +@end + +@implementation ASTextNodeTestDelegate + +- (void)richTextNode:(ASTextNode *)richTextNode tappedLinkAttribute:(NSString *)attribute value:(id)value atPoint:(CGPoint)point textRange:(NSRange)textRange +{ + _tappedLinkAttribute = attribute; + _tappedLinkValue = value; +} + +- (BOOL)richTextNode:(ASTextNode *)richTextNode shouldHighlightLinkAttribute:(NSString *)attribute value:(id)value +{ + return YES; +} + +@end + +@interface ASTextNodeTests : XCTestCase + +@property (nonatomic, readwrite, strong) ASTextNode *textNode; +@property (nonatomic, readwrite, copy) NSAttributedString *attributedString; + +@end + +@implementation ASTextNodeTests + +- (void)setUp +{ + [super setUp]; + _textNode = [[ASTextNode alloc] init]; + + UIFontDescriptor *desc = + [UIFontDescriptor fontDescriptorWithName:@"Didot" size:18]; + NSArray *arr = + @[@{UIFontFeatureTypeIdentifierKey:@(kLetterCaseType), + UIFontFeatureSelectorIdentifierKey:@(kSmallCapsSelector)}]; + desc = + [desc fontDescriptorByAddingAttributes: + @{UIFontDescriptorFeatureSettingsAttribute:arr}]; + UIFont *f = [UIFont fontWithDescriptor:desc size:0]; + NSDictionary *d = @{NSFontAttributeName: f}; + NSMutableAttributedString *mas = + [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." attributes:d]; + NSMutableParagraphStyle *para = [NSMutableParagraphStyle new]; + para.alignment = NSTextAlignmentCenter; + [mas addAttribute:NSParagraphStyleAttributeName value:para range:NSMakeRange(0,mas.length)]; + _attributedString = mas; + _textNode.attributedString = _attributedString; +} + +#pragma mark - ASTextNode + +- (void)testAllocASTextNode +{ + ASTextNode *node = [[ASTextNode alloc] init]; + XCTAssertTrue([[node class] isSubclassOfClass:[ASTextNode class]], @"ASTextNode alloc should return an instance of ASTextNode, instead returned %@", [node class]); +} + +#pragma mark - ASTextNode + +- (void)testSettingTruncationMessage +{ + NSAttributedString *truncation = [[NSAttributedString alloc] initWithString:@"..." attributes:nil]; + _textNode.truncationAttributedString = truncation; + XCTAssertTrue([_textNode.truncationAttributedString isEqualToAttributedString:truncation], @"Failed to set truncation message"); +} + +- (void)testCalculatedSizeIsGreaterThanOrEqualToConstrainedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode sizeToFit:constrainedSize]; + XCTAssertTrue(calculatedSize.width <= constrainedSize.width, @"Calculated width (%f) should be less than or equal to constrained width (%f)", calculatedSize.width, constrainedSize.width); + XCTAssertTrue(calculatedSize.height <= constrainedSize.height, @"Calculated height (%f) should be less than or equal to constrained height (%f)", calculatedSize.height, constrainedSize.height); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedSize +{ + for (NSInteger i = 10; i < 500; i += 50) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode sizeToFit:constrainedSize]; + CGSize recalculatedSize = [_textNode sizeToFit:calculatedSize]; + + XCTAssertTrue(CGSizeEqualToSize(calculatedSize, recalculatedSize), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +- (void)testRecalculationOfSizeIsSameAsOriginallyCalculatedFloatingPointSize +{ + for (CGFloat i = 10; i < 500; i *= 1.3) { + CGSize constrainedSize = CGSizeMake(i, i); + CGSize calculatedSize = [_textNode sizeToFit:constrainedSize]; + CGSize recalculatedSize = [_textNode sizeToFit:calculatedSize]; + + XCTAssertTrue(CGSizeEqualToSize(calculatedSize, recalculatedSize), @"Recalculated size %@ should be same as original size %@", NSStringFromCGSize(recalculatedSize), NSStringFromCGSize(calculatedSize)); + } +} + +- (void)testAccessibility +{ + _textNode.attributedString = _attributedString; + XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); + XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText, @"Should have static text accessibility trait, instead has %llu", _textNode.accessibilityTraits); + + XCTAssertTrue([_textNode.accessibilityLabel isEqualToString:_attributedString.string], @"Accessibility label is incorrectly set to \n%@\n when it should be \n%@\n", _textNode.accessibilityLabel, _attributedString.string); +} + +- (void)testLinkAttribute +{ + NSString *linkAttributeName = @"MockLinkAttributeName"; + NSString *linkAttributeValue = @"MockLinkAttributeValue"; + NSString *linkString = @"Link"; + NSRange linkRange = NSMakeRange(0, linkString.length); + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:linkString attributes:@{ linkAttributeName : linkAttributeValue}]; + _textNode.attributedString = attributedString; + _textNode.linkAttributeNames = @[linkAttributeName]; + + ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; + _textNode.delegate = delegate; + + [_textNode sizeToFit:CGSizeMake(100, 100)]; + NSRange returnedLinkRange; + NSString *returnedAttributeName; + NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange]; + XCTAssertTrue([linkAttributeName isEqualToString:returnedAttributeName], @"Expecting a link attribute name of %@, returned %@", linkAttributeName, returnedAttributeName); + XCTAssertTrue([linkAttributeValue isEqualToString:returnedLinkAttributeValue], @"Expecting a link attribute value of %@, returned %@", linkAttributeValue, returnedLinkAttributeValue); + XCTAssertTrue(NSEqualRanges(linkRange, returnedLinkRange), @"Expected a range of %@, got a link range of %@", NSStringFromRange(linkRange), NSStringFromRange(returnedLinkRange)); +} + +- (void)testTapNotOnALinkAttribute +{ + NSString *linkAttributeName = @"MockLinkAttributeName"; + NSString *linkAttributeValue = @"MockLinkAttributeValue"; + NSString *linkString = @"Link notalink"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:linkString]; + [attributedString addAttribute:linkAttributeName value:linkAttributeValue range:NSMakeRange(0, 4)]; + _textNode.attributedString = attributedString; + _textNode.linkAttributeNames = @[linkAttributeName]; + + ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; + _textNode.delegate = delegate; + + CGSize calculatedSize = [_textNode sizeToFit:CGSizeMake(100, 100)]; + NSRange returnedLinkRange = NSMakeRange(NSNotFound, 0); + NSRange expectedRange = NSMakeRange(NSNotFound, 0); + NSString *returnedAttributeName; + CGPoint pointNearEndOfString = CGPointMake(calculatedSize.width - 3, calculatedSize.height / 2); + NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:pointNearEndOfString attributeName:&returnedAttributeName range:&returnedLinkRange]; + XCTAssertFalse(returnedAttributeName, @"Expecting no link attribute name, returned %@", returnedAttributeName); + XCTAssertFalse(returnedLinkAttributeValue, @"Expecting no link attribute value, returned %@", returnedLinkAttributeValue); + XCTAssertTrue(NSEqualRanges(expectedRange, returnedLinkRange), @"Expected a range of %@, got a link range of %@", NSStringFromRange(expectedRange), NSStringFromRange(returnedLinkRange)); + + XCTAssertFalse(delegate.tappedLinkAttribute, @"Expected the delegate to be told that %@ was tapped, instead it thinks the tapped attribute is %@", linkAttributeName, delegate.tappedLinkAttribute); + XCTAssertFalse(delegate.tappedLinkValue, @"Expected the delegate to be told that the value %@ was tapped, instead it thinks the tapped attribute value is %@", linkAttributeValue, delegate.tappedLinkValue); +} + +@end diff --git a/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm b/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm new file mode 100644 index 0000000000..6ff14e02c3 --- /dev/null +++ b/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm @@ -0,0 +1,137 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASTextNodeTextKitHelpers.h" +#import "ASTextNodeTypes.h" +#import "ASTextNodeWordKerner.h" + +@interface ASTextNodeWordKernerTests : XCTestCase + +@property (nonatomic, readwrite, strong) ASTextNodeWordKerner *layoutManagerDelegate; +@property (nonatomic, readwrite, assign) ASTextKitComponents components; +@property (nonatomic, readwrite, copy) NSAttributedString *attributedString; + +@end + +@implementation ASTextNodeWordKernerTests + +- (void)setUp +{ + _layoutManagerDelegate = [[ASTextNodeWordKerner alloc] init]; + _components.layoutManager.delegate = _layoutManagerDelegate; +} + +- (void)setupTextKitComponentsWithoutWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = nil; + NSString *seedString = @"Hello world"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + _components = ASTextKitComponentsCreate(attributedString, size); +} + +- (void)setupTextKitComponentsWithWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @"Hello world"; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + _components = ASTextKitComponentsCreate(attributedString, size); +} + +- (void)setupTextKitComponentsWithWordKerningDifferentFontSizes +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @" "; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + UIFont *bigFont = [UIFont systemFontOfSize:36]; + UIFont *normalFont = [UIFont systemFontOfSize:12]; + [attributedString addAttribute:NSFontAttributeName value:bigFont range:NSMakeRange(0, 1)]; + [attributedString addAttribute:NSFontAttributeName value:normalFont range:NSMakeRange(1, 1)]; + _components = ASTextKitComponentsCreate(attributedString, size); +} + +- (void)testSomeGlyphsToChangeIfWordKerning +{ + [self setupTextKitComponentsWithWordKerning]; + + NSInteger glyphsToChange = [self _layoutManagerShouldGenerateGlyphs]; + XCTAssertTrue(glyphsToChange > 0, @"Should have changed the properties on some glyphs"); +} + +- (void)testSpaceBoundingBoxForNoWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{NSFontAttributeName : [UIFont systemFontOfSize:12]}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@" " attributes:attributes]; + _components = ASTextKitComponentsCreate(attributedString, size); + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:CGPointZero characterIndex:0]; + XCTAssertTrue(boundingBox.size.width == 4, @"Word kerning shouldn't alter the default width of 4px. Encountered space width was %f", boundingBox.size.width); +} + +- (void)testSpaceBoundingBoxForWordKerning +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5", + NSFontAttributeName : [UIFont systemFontOfSize:12]}; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@" " attributes:attributes]; + _components = ASTextKitComponentsCreate(attributedString, size); + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:CGPointZero characterIndex:0]; + XCTAssertTrue(boundingBox.size.width == 4.5, @"Word kerning shouldn't alter the default width of 4px. Encountered space width was %f", boundingBox.size.width); +} + +- (NSInteger)_layoutManagerShouldGenerateGlyphs +{ + NSRange stringRange = NSMakeRange(0, _components.textStorage.length); + NSRange glyphRange = [_components.layoutManager glyphRangeForCharacterRange:stringRange actualCharacterRange:NULL]; + NSInteger glyphCount = glyphRange.length; + NSUInteger *characterIndexes = (NSUInteger *)malloc(sizeof(NSUInteger) * glyphCount); + for (NSUInteger i=0; i < stringRange.length; i++) { + characterIndexes[i] = i; + } + NSGlyphProperty *glyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount); + CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * glyphCount); + NSInteger glyphsToChange = [_layoutManagerDelegate layoutManager:_components.layoutManager shouldGenerateGlyphs:glyphs properties:glyphProperties characterIndexes:characterIndexes font:NULL forGlyphRange:stringRange]; + free(characterIndexes); + free(glyphProperties); + return glyphsToChange; +} + +- (void)testPerCharacterWordKerning +{ + [self setupTextKitComponentsWithWordKerningDifferentFontSizes]; + CGPoint glyphPosition = CGPointZero; + NSUInteger bigSpaceIndex = 0; + NSUInteger normalSpaceIndex = 1; + CGRect bigBoundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:bigSpaceIndex forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:bigSpaceIndex]; + CGRect normalBoundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:normalSpaceIndex forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:normalSpaceIndex]; + XCTAssertTrue(bigBoundingBox.size.width > normalBoundingBox.size.width, @"Unbolded and bolded spaces should have different kerning"); +} + +- (void)testWordKerningDoesNotAlterGlyphOrigin +{ + CGSize size = CGSizeMake(200, 200); + NSDictionary *attributes = @{ASTextNodeWordKerningAttributeName: @".5"}; + NSString *seedString = @" "; + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:seedString attributes:attributes]; + UIFont *normalFont = [UIFont systemFontOfSize:12]; + [attributedString addAttribute:NSFontAttributeName value:normalFont range:NSMakeRange(0, 1)]; + _components = ASTextKitComponentsCreate(attributedString, size); + + CGPoint glyphPosition = CGPointMake(42, 54); + + CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:glyphPosition characterIndex:0]; + XCTAssertTrue(CGPointEqualToPoint(glyphPosition, boundingBox.origin), @"Word kerning shouldn't alter the origin point of a glyph"); +} + +@end diff --git a/AsyncDisplayKitTests/AsyncDisplayKitTests-Info.plist b/AsyncDisplayKitTests/AsyncDisplayKitTests-Info.plist new file mode 100644 index 0000000000..c317ef5221 --- /dev/null +++ b/AsyncDisplayKitTests/AsyncDisplayKitTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.facebook.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/AsyncDisplayKitTests/AsyncDisplayKitTests-Prefix.pch b/AsyncDisplayKitTests/AsyncDisplayKitTests-Prefix.pch new file mode 100644 index 0000000000..625be4d28b --- /dev/null +++ b/AsyncDisplayKitTests/AsyncDisplayKitTests-Prefix.pch @@ -0,0 +1,9 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/AsyncDisplayKitTests/en.lproj/InfoPlist.strings b/AsyncDisplayKitTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/AsyncDisplayKitTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Base/ASAssert.h b/Base/ASAssert.h new file mode 100644 index 0000000000..33a198d569 --- /dev/null +++ b/Base/ASAssert.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +#import + +#define ASDisplayNodeAssertWithSignalAndLogFunction(condition, description, logFunction, ...) NSAssert(condition, description, ##__VA_ARGS__); +#define ASDisplayNodeCAssertWithSignalAndLogFunction(condition, description, logFunction, ...) NSCAssert(condition, description, ##__VA_ARGS__); +#define ASDisplayNodeAssertWithSignal(condition, description, ...) NSAssert(condition, description, ##__VA_ARGS__) +#define ASDisplayNodeCAssertWithSignal(condition, description, ...) NSCAssert(condition, description, ##__VA_ARGS__) + +#define ASDISPLAYNODE_ASSERTIONS_ENABLED (!defined(NS_BLOCK_ASSERTIONS)) + +#define ASDisplayNodeAssert(...) NSAssert(__VA_ARGS__) +#define ASDisplayNodeCAssert(...) NSCAssert(__VA_ARGS__) + +#define ASDisplayNodeAssertNil(condition, description, ...) ASDisplayNodeAssertWithSignal(!(condition), nil, (description), ##__VA_ARGS__) +#define ASDisplayNodeCAssertNil(condition, description, ...) ASDisplayNodeCAssertWithSignal(!(condition), nil, (description), ##__VA_ARGS__) + +#define ASDisplayNodeAssertNotNil(condition, description, ...) ASDisplayNodeAssertWithSignal((condition), nil, (description), ##__VA_ARGS__) +#define ASDisplayNodeCAssertNotNil(condition, description, ...) ASDisplayNodeCAssertWithSignal((condition), nil, (description), ##__VA_ARGS__) + +#define ASDisplayNodeAssertImplementedBySubclass() ASDisplayNodeAssertWithSignal(NO, nil, @"This method must be implemented by subclass %@", [self class]); +#define ASDisplayNodeAssertNotInstantiable() ASDisplayNodeAssertWithSignal(NO, nil, @"This class is not instantiable."); + +#define ASDisplayNodeAssertMainThread() ASDisplayNodeAssertWithSignal([NSThread isMainThread], nil, @"This method must be called on the main thread") +#define ASDisplayNodeCAssertMainThread() ASDisplayNodeCAssertWithSignal([NSThread isMainThread], nil, @"This function must be called on the main thread") + +#define ASDisplayNodeAssertNotMainThread() ASDisplayNodeAssertWithSignal(![NSThread isMainThread], nil, @"This method must be called off the main thread") +#define ASDisplayNodeCAssertNotMainThread() ASDisplayNodeCAssertWithSignal(![NSThread isMainThread], nil, @"This function must be called off the main thread") + +#define ASDisplayNodeAssertFlag(X) ASDisplayNodeAssertWithSignal((1 == __builtin_popcount(X)), nil, nil) +#define ASDisplayNodeCAssertFlag(X) ASDisplayNodeCAssertWithSignal((1 == __builtin_popcount(X)), nil, nil) + +#define ASDisplayNodeAssertTrue(condition) ASDisplayNodeAssertWithSignal((condition), nil, nil) +#define ASDisplayNodeCAssertTrue(condition) ASDisplayNodeCAssertWithSignal((condition), nil, nil) + +#define ASDisplayNodeAssertFalse(condition) ASDisplayNodeAssertWithSignal(!(condition), nil, nil) +#define ASDisplayNodeCAssertFalse(condition) ASDisplayNodeCAssertWithSignal(!(condition), nil, nil) + +#define ASDisplayNodeFailAssert(description, ...) ASDisplayNodeAssertWithSignal(NO, nil, (description), ##__VA_ARGS__) +#define ASDisplayNodeCFailAssert(description, ...) ASDisplayNodeCAssertWithSignal(NO, nil, (description), ##__VA_ARGS__) diff --git a/Base/ASBaseDefines.h b/Base/ASBaseDefines.h new file mode 100755 index 0000000000..1beeca05fe --- /dev/null +++ b/Base/ASBaseDefines.h @@ -0,0 +1,131 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#pragma once + +// The C++ compiler mangles C function names. extern "C" { /* your C functions */ } prevents this. +// You should wrap all C function prototypes declared in headers with ASDISPLAYNODE_EXTERN_C_BEGIN/END, even if +// they are included only from .m (Objective-C) files. It's common for .m files to start using C++ +// features and become .mm (Objective-C++) files. Always wrapping the prototypes with +// ASDISPLAYNODE_EXTERN_C_BEGIN/END will save someone a headache once they need to do this. You do not need to +// wrap constants, only C functions. See StackOverflow for more details: +// http://stackoverflow.com/questions/1041866/in-c-source-what-is-the-effect-of-extern-c +#ifdef __cplusplus +# define ASDISPLAYNODE_EXTERN_C_BEGIN extern "C" { +# define ASDISPLAYNODE_EXTERN_C_END } +#else +# define ASDISPLAYNODE_EXTERN_C_BEGIN +# define ASDISPLAYNODE_EXTERN_C_END +#endif + +#ifdef __GNUC__ +# define ASDISPLAYNODE_GNUC(major, minor) \ +(__GNUC__ > (major) || (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor))) +#else +# define ASDISPLAYNODE_GNUC(major, minor) 0 +#endif + +#ifndef ASDISPLAYNODE_INLINE +# if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define ASDISPLAYNODE_INLINE static inline +# elif defined (__MWERKS__) || defined (__cplusplus) +# define ASDISPLAYNODE_INLINE static inline +# elif ASDISPLAYNODE_GNUC (3, 0) +# define ASDISPLAYNODE_INLINE static __inline__ __attribute__ ((always_inline)) +# else +# define ASDISPLAYNODE_INLINE static +# endif +#endif + +#ifndef ASDISPLAYNODE_HIDDEN +# if ASDISPLAYNODE_GNUC (4,0) +# define ASDISPLAYNODE_HIDDEN __attribute__ ((visibility ("hidden"))) +# else +# define ASDISPLAYNODE_HIDDEN /* no hidden */ +# endif +#endif + +#ifndef ASDISPLAYNODE_PURE +# if ASDISPLAYNODE_GNUC (3, 0) +# define ASDISPLAYNODE_PURE __attribute__ ((pure)) +# else +# define ASDISPLAYNODE_PURE /* no pure */ +# endif +#endif + +#ifndef ASDISPLAYNODE_WARN_UNUSED +# if ASDISPLAYNODE_GNUC (3, 4) +# define ASDISPLAYNODE_WARN_UNUSED __attribute__ ((warn_unused_result)) +# else +# define ASDISPLAYNODE_WARN_UNUSED /* no warn_unused */ +# endif +#endif + +#ifndef ASDISPLAYNODE_WARN_DEPRECATED +# define ASDISPLAYNODE_WARN_DEPRECATED 1 +#endif + +#ifndef ASDISPLAYNODE_DEPRECATED +# if ASDISPLAYNODE_GNUC (3, 0) && ASDISPLAYNODE_WARN_DEPRECATED +# define ASDISPLAYNODE_DEPRECATED __attribute__ ((deprecated)) +# else +# define ASDISPLAYNODE_DEPRECATED +# endif +#endif + +#if defined (__cplusplus) && defined (__GNUC__) +# define ASDISPLAYNODE_NOTHROW __attribute__ ((nothrow)) +#else +# define ASDISPLAYNODE_NOTHROW +#endif + +#define ARRAY_COUNT(x) sizeof(x) / sizeof(x[0]) + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_CONSUMED +#if __has_feature(attribute_ns_consumed) +#define NS_CONSUMED __attribute__((ns_consumed)) +#else +#define NS_CONSUMED +#endif +#endif + +#ifndef NS_RETURNS_RETAINED +#if __has_feature(attribute_ns_returns_retained) +#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained)) +#else +#define NS_RETURNS_RETAINED +#endif +#endif + +#ifndef CF_RETURNS_RETAINED +#if __has_feature(attribute_cf_returns_retained) +#define CF_RETURNS_RETAINED __attribute__((cf_returns_retained)) +#else +#define CF_RETURNS_RETAINED +#endif +#endif + +#ifndef ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER +#define ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER() \ + do { \ + NSAssert2(NO, @"%@ is not the designated initializer for instances of %@.", NSStringFromSelector(_cmd), NSStringFromClass([self class])); \ + return nil; \ + } while (0) +#endif // ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER + +// It's hard to pass quoted strings via xcodebuild preprocessor define arguments, so we'll convert +// the preprocessor values to strings here. +// +// It takes two steps to do this in gcc as per +// http://gcc.gnu.org/onlinedocs/cpp/Stringification.html +#define ASDISPLAYNODE_TO_STRING(str) #str +#define ASDISPLAYNODE_TO_UNICODE_STRING(str) @ASDISPLAYNODE_TO_STRING(str) diff --git a/Base/ASDisplayNodeExtraIvars.h b/Base/ASDisplayNodeExtraIvars.h new file mode 100644 index 0000000000..c55fc47c65 --- /dev/null +++ b/Base/ASDisplayNodeExtraIvars.h @@ -0,0 +1,10 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +typedef struct _ASDisplayNodeExtraIvars { +} ASDisplayNodeExtraIvars; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..7f627a26c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing to AsyncDisplayKit +We want to make contributing to this project as easy and transparent as +possible, and actively welcome your pull requests. If you run into problems, +please open an issue on GitHub. + +## Pull Requests +1. Fork the repo and create your branch from `master`. +2. If you've added code that should be tested, add tests +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. If you haven't already, complete the Contributor License Agreement ("CLA"). + +## Contributor License Agreement ("CLA") +In order to accept your pull request, we need you to submit a CLA. You only need +to do this once to work on any of Facebook's open source projects. + +Complete your CLA here: + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe +disclosure of security bugs. In those cases, please go through the process +outlined on that page and do not file a public issue. + +## Coding Style +* 2 spaces for indentation rather than tabs + +## License +By contributing to AsyncDisplayKit, you agree that your contributions will be +licensed under its BSD license. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..30472d99ca --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For AsyncDisplayKit software + +Copyright (c) 2014, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PATENTS b/PATENTS new file mode 100644 index 0000000000..3e031107a8 --- /dev/null +++ b/PATENTS @@ -0,0 +1,23 @@ +Additional Grant of Patent Rights + +"Software" means the AsyncDisplayKit software distributed by Facebook, Inc. + +Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable (subject to the termination provision below) license under any +rights in any patent claims owned by Facebook, to make, have made, use, sell, +offer to sell, import, and otherwise transfer the Software. For avoidance of +doubt, no license is granted under Facebook’s rights in any patent claims that +are infringed by (i) modifications to the Software made by you or a third party, +or (ii) the Software in combination with any software or other technology +provided by you or a third party. + +The license granted hereunder will terminate, automatically and without notice, +for anyone that makes any claim (including by filing any lawsuit, assertion or +other action) alleging (a) direct, indirect, or contributory infringement or +inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or +affiliates, whether or not such claim is related to the Software, (ii) by any +party if such claim arises in whole or in part from any software, product or +service of Facebook or any of its subsidiaries or affiliates, whether or not +such claim is related to the Software, or (iii) by any party relating to the +Software; or (b) that any right in any patent claim of Facebook is invalid or +unenforceable. diff --git a/Podfile b/Podfile new file mode 100644 index 0000000000..9def4ace9a --- /dev/null +++ b/Podfile @@ -0,0 +1,5 @@ +platform :ios, '7.0' + +target :'AsyncDisplayKitTests', :exclusive => true do + pod 'OCMock', '~> 2.2' +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000000..d98c0230cd --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,10 @@ +PODS: + - OCMock (2.2.4) + +DEPENDENCIES: + - OCMock (~> 2.2) + +SPEC CHECKSUMS: + OCMock: 6db79185520e24f9f299548f2b8b07e41d881bd5 + +COCOAPODS: 0.33.1 diff --git a/README.md b/README.md new file mode 100644 index 0000000000..e0752fd025 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# AsyncDisplayKit + +Welcome to the AsyncDisplayKit beta! Documentation — including this README — will be fleshed out for the initial public release. Until then, please direct questions and feedback to the [Paper Engineering Community](https://www.facebook.com/groups/551597518288687) group. + +## Installation + +AsyncDisplayKit will be available on [CocoaPods](http://cocoapods.org/). You can manually include it in your project's Podfile: + +`pod 'AsyncDisplayKit', :git => 'https://github.com/facebook/AsyncDisplayKit.git'` + +## Usage + +`#import ` + +## Testing + +AsyncDisplayKit has extensive unit test coverage. You may need to run `pod install` in the root AsyncDisplayKit directory to include OCMock. + +## Contributing + +See the CONTRIBUTING file for how to help out. + +## License + +AsyncDisplayKit is BSD-licensed. We also provide an additional patent grant.