diff --git a/.travis.yml b/.travis.yml index c75f1a0..8c21337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: objective-c -osx_image: xcode9.4 +osx_image: xcode10 script: ./build.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 42669fb..a55ea79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,37 @@ ## Info -**Document version:** 2.10.0 +**Document version:** 2.11.0 -**Last updated:** 08/17/2018 +**Last updated:** 09/03/2018 **Author:** Nolan O'Brien ## History +### 2.11.0 + +- add support for animated images with `TIPImageViewFetchHelper` by supporting `TIPImageContainer` as well as `UIImage` + - to support animated images, implement a `UIView` that adopts `TIPImageFetchable` with `tip_fetchedImageContainer` that can animate the provided `TIPImageContainer` + - update `TIPImageFetchable` + - add `tip_fetchedImageContainer` as optional property + - mark `tip_fetchedImage` as optional + - require at least one of the two methods be implemented to conform to `TIPImageFetchable` + - add helper functions: + - `TIPImageFetchableHasImage` + - `TIPImageFetchableGetImage` and `TIPImageFetchableGetImageContainer` + - `TIPImageFetchableSetImage` and `TIPImageFetchableSetImageContainer` + - update `TIPImageViewFetchHelper` + - add `setImageContainerAsIfLoaded:` + - add `setImageContainerAsIfPlaceholder:` + - update `TIPImageViewFetchHelperDataSource` + - add `tip_imageContainerForFetchHelper:` + - update `TIPImageViewFetchHelperDelegate` + - add `tip_fetchHelper:didUpdateDisplayedImageContainer:fromSourceDimensions:isFinal:` + - deprecate `tip_fetchHelper:didUpdateDisplayedImage:fromSourceDimensions:isFinal:` + - add `tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImageContainer:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:` + - deprecate `tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImage:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:` + ### 2.10.0 - drop support for iOS 7 diff --git a/ImageSpeedComparison/Images.xcassets/AppIcon.appiconset/Contents.json b/ImageSpeedComparison/Images.xcassets/AppIcon.appiconset/Contents.json index b8236c6..19882d5 100644 --- a/ImageSpeedComparison/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/ImageSpeedComparison/Images.xcassets/AppIcon.appiconset/Contents.json @@ -39,6 +39,11 @@ "idiom" : "iphone", "size" : "60x60", "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/TIP Sample App/TwitterSearchViewController.m b/TIP Sample App/TwitterSearchViewController.m index 6428ad8..d6692ba 100644 --- a/TIP Sample App/TwitterSearchViewController.m +++ b/TIP Sample App/TwitterSearchViewController.m @@ -256,7 +256,7 @@ - (BOOL)tip_fetchHelper:(nonnull TIPImageViewFetchHelper *)helper shouldLoadProg return YES; } -//- (BOOL)tip_fetchHelper:(nonnull TIPImageViewFetchHelper *)helper shouldReloadAfterDifferentFetchCompletedWithImage:(nonnull UIImage *)image dimensions:(CGSize)dimensions identifier:(nonnull NSString *)identifier URL:(nonnull NSURL *)URL treatedAsPlaceholder:(BOOL)placeholder manuallyStored:(BOOL)manuallyStored +//- (BOOL)tip_fetchHelper:(nonnull TIPImageViewFetchHelper *)helper shouldReloadAfterDifferentFetchCompletedWithImageContainer:(nonnull TIPImageContainer *)imageContainer dimensions:(CGSize)dimensions identifier:(nonnull NSString *)identifier URL:(nonnull NSURL *)URL treatedAsPlaceholder:(BOOL)placeholder manuallyStored:(BOOL)manuallyStored //{ // //} diff --git a/TwitterImagePipeline.podspec b/TwitterImagePipeline.podspec index 709c223..4f3abe9 100644 --- a/TwitterImagePipeline.podspec +++ b/TwitterImagePipeline.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TwitterImagePipeline' - s.version = '2.10.0' + s.version = '2.11.0' s.summary = 'Twitter Image Pipeline is a robust and performant image loading and caching framework for iOS' s.description = 'Twitter created a framework for image loading/caching in order to fulfill the numerous needs of Twitter for iOS including being fast, safe, modular and versatile.' s.homepage = 'https://github.com/twitter/ios-twitter-logging-service' diff --git a/TwitterImagePipeline.xcodeproj/project.pbxproj b/TwitterImagePipeline.xcodeproj/project.pbxproj index 54adcf1..7bf626a 100644 --- a/TwitterImagePipeline.xcodeproj/project.pbxproj +++ b/TwitterImagePipeline.xcodeproj/project.pbxproj @@ -229,6 +229,9 @@ 8B96C07E1AA930E500C44222 /* TIPImagePipeline.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B96C0761AA930E500C44222 /* TIPImagePipeline.m */; }; 8B96C09E1AA934C300C44222 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B96C09D1AA934C300C44222 /* SystemConfiguration.framework */; }; 8B96C0A11AA934EF00C44222 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B96C0A01AA934EF00C44222 /* libz.dylib */; }; + 8B9845D9216550E600BDFC5C /* TIPImageFetchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B9845D8216550E600BDFC5C /* TIPImageFetchable.m */; }; + 8B9845DA216550F400BDFC5C /* TIPImageFetchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B9845D8216550E600BDFC5C /* TIPImageFetchable.m */; }; + 8B9845DB216550F500BDFC5C /* TIPImageFetchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B9845D8216550E600BDFC5C /* TIPImageFetchable.m */; }; 8B9B6C151E69EBEF00D9E590 /* TweetImageFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B9B6C141E69EBEF00D9E590 /* TweetImageFetchRequest.swift */; }; 8BA232711DD430D70097B1DE /* TIPXWebPCodec.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BA2326F1DD430D60097B1DE /* TIPXWebPCodec.h */; }; 8BA232721DD430D70097B1DE /* TIPXWebPCodec.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BA232701DD430D70097B1DE /* TIPXWebPCodec.m */; }; @@ -322,9 +325,15 @@ 8BE31CB31B9A276F009BC0B2 /* twitterfied.pjpg in Resources */ = {isa = PBXBuildFile; fileRef = 8BE31CB01B9A276F009BC0B2 /* twitterfied.pjpg */; }; 8BE31CB81B9A27DA009BC0B2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B96C0A01AA934EF00C44222 /* libz.dylib */; }; 8BE31CB91B9A27EB009BC0B2 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B96C09D1AA934C300C44222 /* SystemConfiguration.framework */; }; + 8BEEDB242166D436007D8384 /* TIPImagePipelineFetchingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2BE21388D7F007261B7 /* TIPImagePipelineFetchingTests.m */; }; 8BF17B5E1ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF17B5B1ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BF17B601ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF17B5C1ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.m */; }; 8BF17B681ADEE27D004F5CAA /* TIPImageFetchProgressiveLoadingPolicy+StaticClass.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF17B661ADEE27D004F5CAA /* TIPImageFetchProgressiveLoadingPolicy+StaticClass.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF4D2BF21388D7F007261B7 /* TIPImagePipelineFetchingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2BE21388D7F007261B7 /* TIPImagePipelineFetchingTests.m */; }; + 8BF4D2C32138939D007261B7 /* TIPTestsSharedUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF4D2C12138939D007261B7 /* TIPTestsSharedUtils.h */; }; + 8BF4D2C42138939D007261B7 /* TIPTestsSharedUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF4D2C12138939D007261B7 /* TIPTestsSharedUtils.h */; }; + 8BF4D2C52138939D007261B7 /* TIPTestsSharedUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2C22138939D007261B7 /* TIPTestsSharedUtils.m */; }; + 8BF4D2C62138939D007261B7 /* TIPTestsSharedUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF4D2C22138939D007261B7 /* TIPTestsSharedUtils.m */; }; 8BFB14FA1C6F80CA00A4DB02 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BFB14F91C6F80CA00A4DB02 /* UIKit.framework */; }; 8BFB15001C6F80DB00A4DB02 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BFB14F91C6F80CA00A4DB02 /* UIKit.framework */; }; 8BFF17711DF5B4AD005DE734 /* TwitterImagePipeline.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BFF176A1DF5B4AD005DE734 /* TwitterImagePipeline.framework */; }; @@ -566,6 +575,7 @@ 8B96C0941AA9311700C44222 /* TIPImageTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TIPImageTest.m; sourceTree = ""; }; 8B96C09D1AA934C300C44222 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 8B96C0A01AA934EF00C44222 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; + 8B9845D8216550E600BDFC5C /* TIPImageFetchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TIPImageFetchable.m; sourceTree = ""; }; 8B9B6C141E69EBEF00D9E590 /* TweetImageFetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweetImageFetchRequest.swift; sourceTree = ""; }; 8BA2326F1DD430D60097B1DE /* TIPXWebPCodec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TIPXWebPCodec.h; path = Extended/TIPXWebPCodec.h; sourceTree = ""; }; 8BA232701DD430D70097B1DE /* TIPXWebPCodec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TIPXWebPCodec.m; path = Extended/TIPXWebPCodec.m; sourceTree = ""; }; @@ -658,6 +668,9 @@ 8BF17B5B1ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TIPImageFetchProgressiveLoadingPolicies.h; sourceTree = ""; }; 8BF17B5C1ADED888004F5CAA /* TIPImageFetchProgressiveLoadingPolicies.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TIPImageFetchProgressiveLoadingPolicies.m; sourceTree = ""; }; 8BF17B661ADEE27D004F5CAA /* TIPImageFetchProgressiveLoadingPolicy+StaticClass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TIPImageFetchProgressiveLoadingPolicy+StaticClass.h"; sourceTree = ""; }; + 8BF4D2BE21388D7F007261B7 /* TIPImagePipelineFetchingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TIPImagePipelineFetchingTests.m; sourceTree = ""; }; + 8BF4D2C12138939D007261B7 /* TIPTestsSharedUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TIPTestsSharedUtils.h; sourceTree = ""; }; + 8BF4D2C22138939D007261B7 /* TIPTestsSharedUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TIPTestsSharedUtils.m; sourceTree = ""; }; 8BFB14F91C6F80CA00A4DB02 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 8BFBD3471AA77DB2007A08DD /* libTwitterImagePipeline.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTwitterImagePipeline.a; sourceTree = BUILT_PRODUCTS_DIR; }; 8BFBD34A1AA77DB2007A08DD /* TwitterImagePipeline.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TwitterImagePipeline.h; sourceTree = ""; }; @@ -1033,6 +1046,7 @@ 8B6968CF1BC6AC4400ADDAF5 /* TIPImageContainer.h */, 8B6968D01BC6AC4400ADDAF5 /* TIPImageContainer.m */, 8B2547B51FCC70FF007EAAAA /* TIPImageFetchable.h */, + 8B9845D8216550E600BDFC5C /* TIPImageFetchable.m */, 8B725B611AB73A1000786F5E /* TIPImageFetchDelegate.h */, 8B2031D11D6E36FF00E9E88F /* TIPImageFetchDownload.h */, 8B2031D21D6E36FF00E9E88F /* TIPImageFetchDownload.m */, @@ -1075,6 +1089,7 @@ children = ( 8BFBD3571AA77DB2007A08DD /* Supporting Files */, 8B9607AF1C63CF1800C99B71 /* TIPImageFetchDelegateTests.m */, + 8BF4D2BE21388D7F007261B7 /* TIPImagePipelineFetchingTests.m */, 8B4F59931AEEE3CA0071AD95 /* TIPImagePipelineTests.m */, 8B96C0941AA9311700C44222 /* TIPImageTest.m */, 8B4F99961ECA9C5200E3DE74 /* TIPImageViewTests.m */, @@ -1083,6 +1098,8 @@ 8B56927D1D7329BB00E07D32 /* TIPTestImageFetchDownloadInternalWithStubbing.m */, 8BA975621D77E34000601D70 /* TIPTests.h */, 8BA975631D77E34000601D70 /* TIPTests.m */, + 8BF4D2C12138939D007261B7 /* TIPTestsSharedUtils.h */, + 8BF4D2C22138939D007261B7 /* TIPTestsSharedUtils.m */, 8BB118F81D834EC200E75CD9 /* TIPTestURLProtocol.h */, 8BB118F91D834EC200E75CD9 /* TIPTestURLProtocol.m */, 8B0D231F1B0307B300DD4C7B /* TIPUtilitiesTests.m */, @@ -1142,6 +1159,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 8BF4D2C42138939D007261B7 /* TIPTestsSharedUtils.h in Headers */, 8B6511F22135DEB400ED057B /* TIPTestImageFetchDownloadInternalWithStubbing.h in Headers */, 8B6511F32135DEB400ED057B /* TIPTestURLProtocol.h in Headers */, 8B6511F52135DEB400ED057B /* TIPTests.h in Headers */, @@ -1217,6 +1235,7 @@ 8BA9756A1D77E34D00601D70 /* TIPTestImageFetchDownloadInternalWithStubbing.h in Headers */, 8BB118FA1D834EC200E75CD9 /* TIPTestURLProtocol.h in Headers */, 8BA232711DD430D70097B1DE /* TIPXWebPCodec.h in Headers */, + 8BF4D2C32138939D007261B7 /* TIPTestsSharedUtils.h in Headers */, 8BA9756C1D77E34D00601D70 /* TIPTests.h in Headers */, 8B70D9681E7B46BC0082EF39 /* TIPXMP4Codec.h in Headers */, ); @@ -1470,7 +1489,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0940; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = Twitter; TargetAttributes = { 8B63017F1E68F9B400C9A86A = { @@ -1698,6 +1717,7 @@ 8B6511A92135DE7300ED057B /* NSData+TIPAdditions.m in Sources */, 8B6511AA2135DE7300ED057B /* TIPPartialImage.m in Sources */, 8B6511AB2135DE7300ED057B /* TIPImageCodecs.m in Sources */, + 8B9845DB216550F500BDFC5C /* TIPImageFetchable.m in Sources */, 8B6511AC2135DE7300ED057B /* TIPImageTypes.m in Sources */, 8B6511AD2135DE7300ED057B /* TIPSafeOperation.m in Sources */, 8B6511AE2135DE7300ED057B /* TIPTiming.m in Sources */, @@ -1729,6 +1749,8 @@ 8B6511E92135DEB400ED057B /* TIPProblematicImagesTest.m in Sources */, 8B6511EA2135DEB400ED057B /* TIPTestURLProtocol.m in Sources */, 8B6511EC2135DEB400ED057B /* TIPXMP4Codec.m in Sources */, + 8BF4D2C62138939D007261B7 /* TIPTestsSharedUtils.m in Sources */, + 8BEEDB242166D436007D8384 /* TIPImagePipelineFetchingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1758,6 +1780,7 @@ 8B4F99971ECA9C5200E3DE74 /* TIPImageViewTests.m in Sources */, 8BA9756E1D77E34D00601D70 /* TIPUtilitiesTests.m in Sources */, 8BA975661D77E34D00601D70 /* TIPImageFetchDelegateTests.m in Sources */, + 8BF4D2C52138939D007261B7 /* TIPTestsSharedUtils.m in Sources */, 8BA9756B1D77E34D00601D70 /* TIPTestImageFetchDownloadInternalWithStubbing.m in Sources */, 8BA975671D77E34D00601D70 /* TIPImagePipelineTests.m in Sources */, 8BA9756D1D77E34D00601D70 /* TIPTests.m in Sources */, @@ -1765,6 +1788,7 @@ 8BB118FB1D834EC200E75CD9 /* TIPTestURLProtocol.m in Sources */, 8BA232721DD430D70097B1DE /* TIPXWebPCodec.m in Sources */, 8B70D96D1E7B896F0082EF39 /* TIPXMP4Codec.m in Sources */, + 8BF4D2BF21388D7F007261B7 /* TIPImagePipelineFetchingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1818,6 +1842,7 @@ 8B2031D41D6E36FF00E9E88F /* TIPImageFetchDownload.m in Sources */, 8BC2179D1DDF69DB0017B0DA /* TIPImageRenderedCache.m in Sources */, 8BC2179F1DDF69DB0017B0DA /* TIPImageStoreAndMoveOperations.m in Sources */, + 8B9845D9216550E600BDFC5C /* TIPImageFetchable.m in Sources */, 8BC217861DDF69DB0017B0DA /* TIP_ProjectCommon.m in Sources */, 8BC2178C1DDF69DB0017B0DA /* TIPImageCacheEntry.m in Sources */, 8B228B561DD14D1E009E8F6F /* TIPImageCodecs.m in Sources */, @@ -1869,6 +1894,7 @@ 3D1659E0207300C200AA140A /* TIPImagePipelineInspectionResult.m in Sources */, 3D1659E2207300C200AA140A /* TIPImageUtils.m in Sources */, 3D1659C4207300C200AA140A /* NSDictionary+TIPAdditions.m in Sources */, + 8B9845DA216550F400BDFC5C /* TIPImageFetchable.m in Sources */, 3D1659E5207300C200AA140A /* UIImage+TIPAdditions.m in Sources */, 3D1659D5207300C200AA140A /* TIPFileUtils.m in Sources */, 3D1659CF207300C200AA140A /* TIPImageRenderedCache.m in Sources */, @@ -2393,7 +2419,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2.10; + CURRENT_PROJECT_VERSION = 2.11; DEAD_CODE_STRIPPING = NO; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; @@ -2406,6 +2432,7 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", + "TIP_PROJECT_VERSION=$(CURRENT_PROJECT_VERSION)", ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; @@ -2463,7 +2490,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2.10; + CURRENT_PROJECT_VERSION = 2.11; DEAD_CODE_STRIPPING = NO; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = "$(CURRENT_PROJECT_VERSION)"; @@ -2471,6 +2498,7 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/Extended"; GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "TIP_PROJECT_VERSION=$(CURRENT_PROJECT_VERSION)"; GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/TwitterImagePipeline.xcodeproj/xcshareddata/xcschemes/TIP Sample App.xcscheme b/TwitterImagePipeline.xcodeproj/xcshareddata/xcschemes/TIP Sample App.xcscheme index 757d2d9..22b3db7 100644 --- a/TwitterImagePipeline.xcodeproj/xcshareddata/xcschemes/TIP Sample App.xcscheme +++ b/TwitterImagePipeline.xcodeproj/xcshareddata/xcschemes/TIP Sample App.xcscheme @@ -1,6 +1,6 @@ = 1.0 && TIP_PROJECT_VERSION <= 10.0, INVALID_TIP_VERSION); + +#define __TIP_VERSION(version) @"" #version +#define _TIP_VERSION(version) __TIP_VERSION( version ) +#define TIP_VERSION() _TIP_VERSION( TIP_PROJECT_VERSION ) + + return TIP_VERSION(); } void TIPSwizzle(Class cls, SEL originalSelector, SEL swizzledSelector) diff --git a/TwitterImagePipeline/TIPGlobalConfiguration.m b/TwitterImagePipeline/TIPGlobalConfiguration.m index 5f32e92..5e51d58 100644 --- a/TwitterImagePipeline/TIPGlobalConfiguration.m +++ b/TwitterImagePipeline/TIPGlobalConfiguration.m @@ -644,9 +644,13 @@ - (void)setLogger:(nullable id)logger TIPAssert(imageFetchDownloadProvider != nil); id download = [imageFetchDownloadProvider imageFetchDownloadWithContext:context]; if (context != download.context) { + NSDictionary *userInfo; + if (imageFetchDownloadProvider) { + userInfo = @{ @"className" : NSStringFromClass([imageFetchDownloadProvider class]) }; + } @throw [NSException exceptionWithName:TIPImageFetchDownloadConstructorExceptionName reason:@"TIPImageFetchDownload did not adhere to protocol requirements!" - userInfo:@{ @"className" : NSStringFromClass([imageFetchDownloadProvider class]) }]; + userInfo:userInfo]; } return download; } diff --git a/TwitterImagePipeline/TIPImageFetchOperation.m b/TwitterImagePipeline/TIPImageFetchOperation.m index 58bfcde..4511784 100644 --- a/TwitterImagePipeline/TIPImageFetchOperation.m +++ b/TwitterImagePipeline/TIPImageFetchOperation.m @@ -784,6 +784,9 @@ - (TIPImageDiskCacheTemporaryFile *)regenerateImageDownloadTemporaryFileForImage - (void)imageDownloadDidStart:(id)context { _background_postDidStartDownload(self); + const TIPPartialImage *partialImage = self->_networkContext.imageDownloadRequest.imageDownloadPartialImageForResuming; + const float progress = partialImage ? partialImage.progress : 0.0f; + _background_updateProgress(self, progress); } - (void)imageDownload:(id)context didResetFromPartialImage:(TIPPartialImage *)oldPartialImage diff --git a/TwitterImagePipeline/TIPImageFetchable.h b/TwitterImagePipeline/TIPImageFetchable.h index bf3ef06..1a4427c 100644 --- a/TwitterImagePipeline/TIPImageFetchable.h +++ b/TwitterImagePipeline/TIPImageFetchable.h @@ -10,20 +10,48 @@ NS_ASSUME_NONNULL_BEGIN +@class TIPImageContainer; +@class UIImage; +@class UIView; + /** - A protocol for enabling a `UIView` to become compliant with `TIPImageViewFetchHelper` + A protocol for enabling a `UIView` to become compliant with `TIPImageViewFetchHelper`. + At least one of `tip_fetchedImage` or `tip_fetchedImageContainer` must be implemented. */ @protocol TIPImageFetchable -@required +@optional + +/** + This property is to set the underlying image of the view for display. + For example, a view can implement `TIPImageFetchable` such that + the `tip_fetchedImageContainer` can show an animated image since that information + is encapsulated in `TIPImageContainer`. + @note `tip_fetchedImageContainer` takes precedent over `tip_fetchImage` + */ +@property (nonatomic, readwrite, nullable) TIPImageContainer *tip_fetchedImageContainer; /** This property is to set the underlying image of the view for display. For example, __TIP__ implements `TIPImageFetchable` on `UIImageView` such that the `tip_fetchedImage` just redirects to `UIImageView` class' `image` property. + @note `tip_fetchedImageContainer` takes precedent over `tip_fetchImage` */ @property (nonatomic, readwrite, nullable) UIImage *tip_fetchedImage; @end +/** does the _fetchable_ have an image (via `tip_fetchedImageContainer` or `tip_fetchedImage`) */ +FOUNDATION_EXTERN BOOL TIPImageFetchableHasImage(id __nullable fetchable); + +/** get the fetched image from the _fetchable_ (via `tip_fetchedImageContainer` or `tip_fetchedImage`) */ +FOUNDATION_EXTERN UIImage * __nullable TIPImageFetchableGetImage(id __nullable fetchable); +/** get the fetched image with container from the _fetchable_ (via `tip_fetchedImageContainer` or `tip_fetchedImage`) */ +FOUNDATION_EXTERN TIPImageContainer * __nullable TIPImageFetchableGetImageContainer(id __nullable fetchable); + +/** set the _fetchable_ fetch image (via `tip_fetchedImageContainer` or `tip_fetchedImage`) */ +FOUNDATION_EXTERN void TIPImageFetchableSetImage(id __nullable fetchable, UIImage * __nullable image); +/** set the _fetchable_ fetch image with a container (via `tip_fetchedImageContainer` or `tip_fetchedImage`) */ +FOUNDATION_EXTERN void TIPImageFetchableSetImageContainer(id __nullable fetchable, TIPImageContainer * __nullable imageContainer); + NS_ASSUME_NONNULL_END diff --git a/TwitterImagePipeline/TIPImageFetchable.m b/TwitterImagePipeline/TIPImageFetchable.m new file mode 100644 index 0000000..68dc236 --- /dev/null +++ b/TwitterImagePipeline/TIPImageFetchable.m @@ -0,0 +1,111 @@ +// +// TIPImageFetchable.m +// TwitterImagePipeline +// +// Created on 09/03/18. +// Copyright © 2018 Twitter. All rights reserved. +// + +#import + +#import "TIP_Project.h" +#import "TIPImageContainer.h" +#import "TIPImageFetchable.h" + +NS_ASSUME_NONNULL_BEGIN + +BOOL TIPImageFetchableHasImage(id __nullable fetchable) +{ + if (!fetchable) { + return NO; + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImageContainer)]) { + return (fetchable.tip_fetchedImageContainer != nil); + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImage)]) { + return (fetchable.tip_fetchedImage != nil); + } + + TIPAssertNever(); + return NO; +} + +UIImage * __nullable TIPImageFetchableGetImage(id __nullable fetchable) +{ + if (!fetchable) { + return nil; + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImageContainer)]) { + return fetchable.tip_fetchedImageContainer.image; + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImage)]) { + return fetchable.tip_fetchedImage; + } + + TIPAssertNever(); + return nil; +} + +TIPImageContainer * __nullable TIPImageFetchableGetImageContainer(id __nullable fetchable) +{ + if (!fetchable) { + return nil; + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImageContainer)]) { + return fetchable.tip_fetchedImageContainer; + } + + if ([fetchable respondsToSelector:@selector(tip_fetchedImage)]) { + UIImage *image = fetchable.tip_fetchedImage; + return (image) ? [[TIPImageContainer alloc] initWithImage:image] : nil; + } + + TIPAssertNever(); + return nil; +} + +void TIPImageFetchableSetImage(id __nullable fetchable, UIImage * __nullable image) +{ + if (!fetchable) { + return; + } + + if ([fetchable respondsToSelector:@selector(setTip_fetchedImageContainer:)]) { + TIPImageContainer *container = (image) ? [[TIPImageContainer alloc] initWithImage:image] : nil; + fetchable.tip_fetchedImageContainer = container; + return; + } + + if ([fetchable respondsToSelector:@selector(setTip_fetchedImage:)]) { + fetchable.tip_fetchedImage = image; + return; + } + + TIPAssertNever(); +} + +void TIPImageFetchableSetImageContainer(id __nullable fetchable, TIPImageContainer * __nullable imageContainer) +{ + if (!fetchable) { + return; + } + + if ([fetchable respondsToSelector:@selector(setTip_fetchedImageContainer:)]) { + fetchable.tip_fetchedImageContainer = imageContainer; + return; + } + + if ([fetchable respondsToSelector:@selector(setTip_fetchedImage:)]) { + fetchable.tip_fetchedImage = imageContainer.image; + return; + } + + TIPAssertNever(); +} + +NS_ASSUME_NONNULL_END diff --git a/TwitterImagePipeline/TIPImageUtils.m b/TwitterImagePipeline/TIPImageUtils.m index 91b84d9..f22f81c 100644 --- a/TwitterImagePipeline/TIPImageUtils.m +++ b/TwitterImagePipeline/TIPImageUtils.m @@ -36,7 +36,11 @@ @implementation TIPRenderImageFormatInternal - (instancetype)initWithRendererFormat:(UIGraphicsImageRendererFormat *)format { if (self = [self init]) { - _prefersExtendedRange = format.prefersExtendedRange; + if (tip_available_ios_12) { + _prefersExtendedRange = (format.preferredRange == UIGraphicsImageRendererFormatRangeExtended); + } else { + _prefersExtendedRange = format.prefersExtendedRange; + } _opaque = format.opaque; _scale = format.scale; } @@ -434,6 +438,11 @@ TIPImageRenderBlock __attribute__((noescape)) renderBlock) // Prep the format mutable object TIPRenderImageFormatInternal *formatInternal = [[TIPRenderImageFormatInternal alloc] initWithRendererFormat:format]; formatInternal.renderSize = size; + if (tip_available_ios_12) { + if (sourceImage) { + formatInternal.prefersExtendedRange = sourceImage.tip_usesWideGamutColorSpace; + } + } // Format the format object formatBlock(formatInternal); @@ -442,7 +451,10 @@ TIPImageRenderBlock __attribute__((noescape)) renderBlock) if (format.opaque != formatInternal.opaque) { format.opaque = formatInternal.opaque; } - if (format.prefersExtendedRange != formatInternal.prefersExtendedRange) { + if (tip_available_ios_12) { + format.preferredRange = (formatInternal.prefersExtendedRange) ? UIGraphicsImageRendererFormatRangeExtended : UIGraphicsImageRendererFormatRangeStandard; + format.prefersExtendedRange = formatInternal.prefersExtendedRange; + } else { format.prefersExtendedRange = formatInternal.prefersExtendedRange; } if (format.scale != formatInternal.scale) { diff --git a/TwitterImagePipeline/TIPImageViewFetchHelper.h b/TwitterImagePipeline/TIPImageViewFetchHelper.h index 69a7089..8d29c98 100644 --- a/TwitterImagePipeline/TIPImageViewFetchHelper.h +++ b/TwitterImagePipeline/TIPImageViewFetchHelper.h @@ -102,10 +102,14 @@ typedef NS_ENUM(NSInteger, TIPImageViewDisappearanceBehavior) /** set the image, as if it was loaded from a fetch */ - (void)setImageAsIfLoaded:(UIImage *)image; +/** set the image container, as if it was loaded from a fetch */ +- (void)setImageContainerAsIfLoaded:(TIPImageContainer *)imageContainer; /** mark the image as loaded from a fetch */ - (void)markAsIfLoaded; /** set the image, as if it was a placeholder image */ - (void)setImageAsIfPlaceholder:(UIImage *)image; +/** set the image container, as if it was a placeholder image */ +- (void)setImageContainerAsIfPlaceholder:(TIPImageContainer *)imageContainer; /** mark the image as a placeholder */ - (void)markAsIfPlaceholder; @@ -179,7 +183,7 @@ FOUNDATION_EXTERN NSString * const TIPImageViewDidUpdateDebugInfoVisibilityNotif /** Data source protocol for `TIPImageViewFetchHelper` Selection order: - 1. TIPImageContainer (TODO) + 1. TIPImageContainer 2. UIImage 3. TIPImageFetchRequest 4. NSURL @@ -191,6 +195,9 @@ FOUNDATION_EXTERN NSString * const TIPImageViewDidUpdateDebugInfoVisibilityNotif // Chosen in order +/** load from a static `TIPImageContainer` */ +- (nullable TIPImageContainer *)tip_imageContainerForFetchHelper:(TIPImageViewFetchHelper *)helper; + /** load from a static `UIImage` */ - (nullable UIImage *)tip_imageForFetchHelper:(TIPImageViewFetchHelper *)helper; @@ -258,7 +265,7 @@ FOUNDATION_EXTERN NSString * const TIPImageViewDidUpdateDebugInfoVisibilityNotif Has automatic/default behavior, call super to utilize auto behavior */ - (BOOL)tip_fetchHelper:(TIPImageViewFetchHelper *)helper - shouldReloadAfterDifferentFetchCompletedWithImage:(UIImage *)image + shouldReloadAfterDifferentFetchCompletedWithImageContainer:(TIPImageContainer *)imageContainer dimensions:(CGSize)dimensions identifier:(NSString *)identifier URL:(NSURL *)URL @@ -272,8 +279,8 @@ FOUNDATION_EXTERN NSString * const TIPImageViewDidUpdateDebugInfoVisibilityNotif - (void)tip_fetchHelperDidStartLoading:(TIPImageViewFetchHelper *)helper; /** fetch did update progress */ - (void)tip_fetchHelper:(TIPImageViewFetchHelper *)helper didUpdateProgress:(float)progress; -/** fetch did update displayed image */ -- (void)tip_fetchHelper:(TIPImageViewFetchHelper *)helper didUpdateDisplayedImage:(UIImage *)image fromSourceDimensions:(CGSize)size isFinal:(BOOL)isFinal; +/** fetch did update displayed image with a `TIPImageContainer` */ +- (void)tip_fetchHelper:(TIPImageViewFetchHelper *)helper didUpdateDisplayedImageContainer:(TIPImageContainer *)imageContainer fromSourceDimensions:(CGSize)size isFinal:(BOOL)isFinal; /** fetch did load final image */ - (void)tip_fetchHelper:(TIPImageViewFetchHelper *)helper didLoadFinalImageFromSource:(TIPImageLoadSource)source; /** fetch did fail */ @@ -283,6 +290,28 @@ FOUNDATION_EXTERN NSString * const TIPImageViewDidUpdateDebugInfoVisibilityNotif /** fetch did start loading from the network */ - (void)tip_fetchHelperDidStartLoadingFromNetwork:(TIPImageViewFetchHelper *)helper; + +#pragma mark Deprecated + +/** + should reload after a different fetch completed? + Has automatic/default behavior, call super to utilize auto behavior + @warning deprecated callback, implement `tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImage:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:` instead + */ +- (BOOL)tip_fetchHelper:(TIPImageViewFetchHelper *)helper + shouldReloadAfterDifferentFetchCompletedWithImage:(UIImage *)image + dimensions:(CGSize)dimensions + identifier:(NSString *)identifier + URL:(NSURL *)URL + treatedAsPlaceholder:(BOOL)placeholder + manuallyStored:(BOOL)manuallyStored __attribute__((deprecated("implement `tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImage:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:` instead"))); + +/** + fetch did update displayed image + @warning deprecated callback, implement `tip_fetchHelper:didUpdateDisplayedImageContainer:fromSourceDimensions:isFinal:` instead + */ +- (void)tip_fetchHelper:(TIPImageViewFetchHelper *)helper didUpdateDisplayedImage:(UIImage *)image fromSourceDimensions:(CGSize)size isFinal:(BOOL)isFinal __attribute__((deprecated("implement `tip_fetchHelper:didUpdateDisplayedImageContainer:fromSourceDimensions:isFinal:` instead"))); + @end NS_ASSUME_NONNULL_END diff --git a/TwitterImagePipeline/TIPImageViewFetchHelper.m b/TwitterImagePipeline/TIPImageViewFetchHelper.m index 6f03b33..0d76248 100644 --- a/TwitterImagePipeline/TIPImageViewFetchHelper.m +++ b/TwitterImagePipeline/TIPImageViewFetchHelper.m @@ -67,7 +67,7 @@ static BOOL _shouldLoadProgressively(SELF_ARG, NSString *imageType, CGSize originalDimensions); static BOOL _shouldReloadAfterDifferentFetchCompleted(SELF_ARG, - UIImage *image, + TIPImageContainer *image, CGSize dimensions, NSString *identifier, NSURL *URL, @@ -77,10 +77,10 @@ static BOOL _shouldReloadAfterDifferentFetchCompleted(SELF_ARG, static void _didStartLoading(SELF_ARG); static void _didUpdateProgress(SELF_ARG, float progress); -static void _didUpdateDisplayedImage(SELF_ARG, - UIImage *image, - CGSize sourceDimensions, - BOOL isFinal); +static void _didUpdateDisplayedImageContainer(SELF_ARG, + TIPImageContainer *imageContainer, + CGSize sourceDimensions, + BOOL isFinal); static void _didLoadFinalImage(SELF_ARG, TIPImageLoadSource source); static void _didFailToLoadFinalImage(SELF_ARG, @@ -187,7 +187,7 @@ static void _setDelegate(SELF_ARG, - (void)setFetchView:(nullable UIView *)fetchView { - TIPAssert(!fetchView || [fetchView respondsToSelector:@selector(setTip_fetchedImage:)]); + TIPAssert(!fetchView || [fetchView respondsToSelector:@selector(setTip_fetchedImage:)] || [fetchView respondsToSelector:@selector(setTip_fetchedImageContainer:)]); UIView *oldView = _fetchView; if (oldView != fetchView) { @@ -231,7 +231,7 @@ - (void)cancelFetchRequest - (void)clearImage { - _resetImage(self, nil /*image*/); + _resetImage(self, nil /*imageContainer*/); } - (void)reload @@ -242,16 +242,22 @@ - (void)reload #pragma mark Override methods - (void)setImageAsIfLoaded:(UIImage *)image +{ + TIPImageContainer *container = (image) ? [[TIPImageContainer alloc] initWithImage:image] : nil; + [self setImageContainerAsIfLoaded:container]; +} + +- (void)setImageContainerAsIfLoaded:(TIPImageContainer *)imageContainer { [self cancelFetchRequest]; _startObservingImagePipeline(self, nil /*image pipeline*/); _markAsIfLoaded(self); - self.fetchView.tip_fetchedImage = image; + TIPImageFetchableSetImageContainer(self.fetchView, imageContainer); } - (void)markAsIfLoaded { - if (self.fetchView.tip_fetchedImage) { + if (TIPImageFetchableHasImage(self.fetchView)) { _markAsIfLoaded(self); } } @@ -268,16 +274,22 @@ static void _markAsIfLoaded(SELF_ARG) } - (void)setImageAsIfPlaceholder:(UIImage *)image +{ + TIPImageContainer *container = (image) ? [[TIPImageContainer alloc] initWithImage:image] : nil; + [self setImageContainerAsIfPlaceholder:container]; +} + +- (void)setImageContainerAsIfPlaceholder:(TIPImageContainer *)imageContainer { [self cancelFetchRequest]; _startObservingImagePipeline(self, nil /*image pipeline*/); _markAsIfPlaceholder(self); - self.fetchView.tip_fetchedImage = image; + TIPImageFetchableSetImageContainer(self.fetchView, imageContainer); } - (void)markAsIfPlaceholder { - if (self.fetchView.tip_fetchedImage) { + if (TIPImageFetchableHasImage(self.fetchView)) { _markAsIfPlaceholder(self); } } @@ -524,7 +536,7 @@ static BOOL _shouldLoadProgressively(SELF_ARG, } static BOOL _shouldReloadAfterDifferentFetchCompleted(SELF_ARG, - UIImage *image, + TIPImageContainer *imageContainer, CGSize dimensions, NSString *identifier, NSURL *URL, @@ -536,18 +548,29 @@ static BOOL _shouldReloadAfterDifferentFetchCompleted(SELF_ARG, } id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImage:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:)]) { + if ([delegate respondsToSelector:@selector(tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImageContainer:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:)]) { return [delegate tip_fetchHelper:self - shouldReloadAfterDifferentFetchCompletedWithImage:image + shouldReloadAfterDifferentFetchCompletedWithImageContainer:imageContainer dimensions:dimensions identifier:identifier URL:URL treatedAsPlaceholder:placeholder manuallyStored:manuallyStored]; + } else if ([delegate respondsToSelector:@selector(tip_fetchHelper:shouldReloadAfterDifferentFetchCompletedWithImage:dimensions:identifier:URL:treatedAsPlaceholder:manuallyStored:)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [delegate tip_fetchHelper:self + shouldReloadAfterDifferentFetchCompletedWithImage:imageContainer.image + dimensions:dimensions + identifier:identifier + URL:URL + treatedAsPlaceholder:placeholder + manuallyStored:manuallyStored]; +#pragma clang diagnostic pop } id request = self.fetchRequest; - if (!self.fetchView.tip_fetchedImage && [request.imageURL isEqual:URL]) { + if (!TIPImageFetchableHasImage(self.fetchView) && [request.imageURL isEqual:URL]) { // auto handle when the image loaded someplace else return YES; } @@ -585,21 +608,29 @@ static void _didUpdateProgress(SELF_ARG, } } -static void _didUpdateDisplayedImage(SELF_ARG, - UIImage *image, - CGSize sourceDimensions, - BOOL isFinal) +static void _didUpdateDisplayedImageContainer(SELF_ARG, + TIPImageContainer *imageContainer, + CGSize sourceDimensions, + BOOL isFinal) { if (!self) { return; } id delegate = self.delegate; - if ([delegate respondsToSelector:@selector(tip_fetchHelper:didUpdateDisplayedImage:fromSourceDimensions:isFinal:)]) { + if ([delegate respondsToSelector:@selector(tip_fetchHelper:didUpdateDisplayedImageContainer:fromSourceDimensions:isFinal:)]) { + [delegate tip_fetchHelper:self + didUpdateDisplayedImageContainer:imageContainer + fromSourceDimensions:sourceDimensions + isFinal:isFinal]; + } else if ([delegate respondsToSelector:@selector(tip_fetchHelper:didUpdateDisplayedImage:fromSourceDimensions:isFinal:)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [delegate tip_fetchHelper:self - didUpdateDisplayedImage:image + didUpdateDisplayedImage:imageContainer.image fromSourceDimensions:sourceDimensions isFinal:isFinal]; +#pragma clang diagnostic pop } } @@ -664,7 +695,7 @@ - (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op continueLoading = !!_shouldContinueLoading(self, previewResult); _update(self, - previewResult.imageContainer.image, + previewResult.imageContainer, previewResult.imageOriginalDimensions, previewResult.imageURL, previewResult.imageSource, @@ -710,7 +741,7 @@ - (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op } _update(self, - progressiveResult.imageContainer.image, + progressiveResult.imageContainer, op.networkImageOriginalDimensions /*sourceImageDimensions*/, progressiveResult.imageURL, progressiveResult.imageSource, @@ -732,7 +763,7 @@ - (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFirstAnimate } _update(self, - progressiveResult.imageContainer.image, + progressiveResult.imageContainer, op.networkImageOriginalDimensions /*sourceImageDimensions*/, op.request.imageURL, progressiveResult.imageSource, @@ -769,7 +800,7 @@ - (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFinalImage:( _fetchOperation = nil; _update(self, - imageContainer.image, + imageContainer, finalResult.imageOriginalDimensions /*sourceImageDimensions*/, finalResult.imageURL, finalResult.imageSource, @@ -854,7 +885,7 @@ static void _prep(SELF_ARG) object:nil]; [self _tip_didUpdateDebugVisibility]; self->_fetchDisappearanceBehavior = TIPImageViewDisappearanceBehaviorCancelImageFetch; - _resetImage(self, nil /*image*/); + _resetImage(self, nil /*imageContainer*/); } static BOOL _resizeRequestIfNeeded(SELF_ARG) @@ -894,7 +925,7 @@ static BOOL _resizeRequestIfNeeded(SELF_ARG) } static void _update(SELF_ARG, - UIImage * __nullable image, + TIPImageContainer * __nullable imageContainer, CGSize sourceDimensions, NSURL * __nullable URL, TIPImageLoadSource source, @@ -930,7 +961,7 @@ static void _update(SELF_ARG, self->_loadedImageType = [type copy]; self.fetchError = error; self.fetchMetrics = metrics; - if (metrics && image) { + if (metrics && imageContainer) { self.fetchResultDimensions = sourceDimensions; } else { self.fetchResultDimensions = CGSizeZero; @@ -941,15 +972,15 @@ static void _update(SELF_ARG, self.fetchProgress = progress; didUpdateProgress = YES; } - self.fetchView.tip_fetchedImage = image; + TIPImageFetchableSetImageContainer(self.fetchView, imageContainer); if (didUpdateProgress) { _didUpdateProgress(self, progress); } - if (image) { - _didUpdateDisplayedImage(self, - image, - sourceDimensions, - (final || scaled) /*isFinal*/); + if (imageContainer) { + _didUpdateDisplayedImageContainer(self, + imageContainer, + sourceDimensions, + (final || scaled) /*isFinal*/); } if (final || scaled) { _didLoadFinalImage(self, source); @@ -961,7 +992,7 @@ static void _update(SELF_ARG, } static void _resetImage(SELF_ARG, - UIImage * __nullable image) + TIPImageContainer * __nullable imageContainer) { if (!self) { return; @@ -970,12 +1001,12 @@ static void _resetImage(SELF_ARG, _cancelFetch(self); _startObservingImagePipeline(self, nil /*image pipelines*/); _update(self, - image, + imageContainer, CGSizeZero /*sourceDimensions*/, nil /*URL*/, TIPImageLoadSourceUnknown, nil /*image type*/, - (image != nil) ? 1.f : 0.f /*progress*/, + (imageContainer != nil) ? 1.f : 0.f /*progress*/, nil /*error*/, nil /*metrics*/, NO /*final*/, @@ -1007,11 +1038,19 @@ static void _refetch(SELF_ARG, if (TIPIsViewVisible(fetchView) || self->_flags.transitioningAppearance) { const CGSize size = fetchView.bounds.size; if (size.width > 0 && size.height > 0) { - if (!fetchView.tip_fetchedImage || !self->_flags.isLoadedImageFinal) { + if (!TIPImageFetchableHasImage(fetchView) || !self->_flags.isLoadedImageFinal) { id dataSource = self.dataSource; // Attempt static load first + if ([dataSource respondsToSelector:@selector(tip_imageContainerForFetchHelper:)]) { + TIPImageContainer *container = [dataSource tip_imageContainerForFetchHelper:self]; + if (container) { + [self setImageContainerAsIfLoaded:container]; + return; + } + } + if ([dataSource respondsToSelector:@selector(tip_imageForFetchHelper:)]) { UIImage *image = [dataSource tip_imageForFetchHelper:self]; if (image) { @@ -1119,10 +1158,9 @@ - (void)_tip_imageDidUpdate:(NSNotification *)note NSURL *URL = userInfo[TIPImagePipelineImageURLNotificationKey]; CGSize dimensions = [(NSValue *)userInfo[TIPImagePipelineImageDimensionsNotificationKey] CGSizeValue]; TIPImageContainer *container = userInfo[TIPImagePipelineImageContainerNotificationKey]; - UIImage *image = container.image; const BOOL shouldReload = _shouldReloadAfterDifferentFetchCompleted(self, - image, + container, dimensions, identifier, URL, diff --git a/TwitterImagePipeline/UIImage+TIPAdditions.h b/TwitterImagePipeline/UIImage+TIPAdditions.h index 3cd8b84..b0d8e4c 100644 --- a/TwitterImagePipeline/UIImage+TIPAdditions.h +++ b/TwitterImagePipeline/UIImage+TIPAdditions.h @@ -102,10 +102,15 @@ NS_ASSUME_NONNULL_BEGIN Return a copy of the image scaled @param targetDimensions the target size in pixels to scale to. @param targetContentMode the target `UIViewContentMode` used to confine the scaling + @param decode decode the image to memory, default is `YES` @note only _targetContentMode_ values that have `UIViewContentModeScale*` will be scaled (others are just positional and do not scale) @warning there is a bug in Apple's frameworks that can yield a `nil` image when scaling. The issue is years old and there are many radars against it (for example #33057552 and #22097047). Rather than expose a pain point of this method potentially returning `nil`, this method will just return `self` in the case that the bug is triggered. */ -- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions contentMode:(UIViewContentMode)targetContentMode; +- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions + contentMode:(UIViewContentMode)targetContentMode + decode:(BOOL)decode; +- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions + contentMode:(UIViewContentMode)targetContentMode; /** Return a copy of the `UIImage` but transformed such that its `imageOrientation` is @@ -114,6 +119,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (UIImage *)tip_orientationAdjustedImage; +/** + Return a copy of the `UIImage` with the `size` and `scale` adjusted to a new _scale_ while + preserving the `tip_dimensions`. + If _scale_ and `scale` match, returns `self` + */ +- (UIImage *)tip_imageByUpdatingScale:(CGFloat)scale; + /** Return a copy of the image backed by a `CGImage` (instead of a `CIImage`) @param error The error if one was encountered @@ -172,6 +184,15 @@ NS_ASSUME_NONNULL_BEGIN durations:(out NSArray * __nullable * __nullable)durationsOut loopCount:(out NSUInteger * __nullable)loopCountOut; +/** + Construct an image without RAM impact of loading the full image before scaling + @param fileURL the file path as an `NSURL` to load the image with + @param thumbnailMaximumDimension the dimension limitation to scale the image down to (but not up!) + @return an `UIImage` or `nil` if there was an error + */ ++ (nullable UIImage *)tip_thumbnailImageWithFileURL:(NSURL *)fileURL + thumbnailMaximumDimension:(CGFloat)thumbnailMaximumDimension; + #pragma mark Encode Methods /** diff --git a/TwitterImagePipeline/UIImage+TIPAdditions.m b/TwitterImagePipeline/UIImage+TIPAdditions.m index 871adc1..2214d9d 100644 --- a/TwitterImagePipeline/UIImage+TIPAdditions.m +++ b/TwitterImagePipeline/UIImage+TIPAdditions.m @@ -313,7 +313,17 @@ - (nullable UIImage *)tip_imageWithRenderFormatting:(nullable TIPImageRenderForm return outImage; } -- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions contentMode:(UIViewContentMode)targetContentMode +- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions + contentMode:(UIViewContentMode)targetContentMode +{ + return [self tip_scaledImageWithTargetDimensions:targetDimensions + contentMode:targetContentMode + decode:YES]; +} + +- (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions + contentMode:(UIViewContentMode)targetContentMode + decode:(BOOL)decode { const CGSize dimensions = [self tip_dimensions]; const CGSize scaledTargetDimensions = TIPSizeGreaterThanZero(targetDimensions) ? TIPDimensionsScaledToTargetSizing(dimensions, targetDimensions, targetContentMode) : CGSizeZero; @@ -322,32 +332,29 @@ - (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions conten // If we have a target size and the target size is not the same as our source image's size, draw the resized image if (TIPSizeGreaterThanZero(scaledTargetDimensions) && !CGSizeEqualToSize(dimensions, scaledTargetDimensions)) { - // OK, so, to provide some context: - // - // The UIKit and CoreGraphics mechanisms for scaling occasionally yield a `nil` image. - // We cannot repro locally, but externally, it is clearly an issue that affects users. - // It has noticeably upticked in iOS 11 as well. - // - // There are numerous radars out there against this, including #33057552 and #22097047. - // It has not been fixed in years and we see no reason to expect a fix in the future. - // - // Rather than incur complexity of implementing our own custom pixel buffer scaling - // as a fallback, we'll instead just return `self` unscaled. This is not ideal, - // but returning a `nil` image is crash prone while returning the image unscaled - // would merely be prone to a performance hit. // scale with UIKit at screen scale image = _UIKitScale(self, scaledTargetDimensions, 0 /*auto scale*/); + // image = _CoreGraphicsScale(self, scaledTargetDimensions, 0 /*auto scale*/); } else { image = self; } - if (image == self) { - [image tip_decode]; - } - + // OK, so, to provide some context: + // + // The UIKit and CoreGraphics mechanisms for scaling occasionally yield a `nil` image. + // We cannot repro locally, but externally, it is clearly an issue that affects users. + // It has noticeably upticked in iOS 11 as well. + // + // There are numerous radars out there against this, including #33057552 and #22097047. + // It has not been fixed in years and we see no reason to expect a fix in the future. + // + // Rather than incur complexity of implementing our own custom pixel buffer scaling + // as a fallback, we'll instead just return `self` unscaled. This is not ideal, + // but returning a `nil` image is crash prone while returning the image unscaled + // would merely be prone to a performance hit. if (!image) { NSDictionary *userInfo = @{ TIPProblemInfoKeyImageDimensions : [NSValue valueWithCGSize:dimensions], @@ -362,6 +369,10 @@ - (UIImage *)tip_scaledImageWithTargetDimensions:(CGSize)targetDimensions conten } TIPAssert(image != nil); + if (decode) { + [image tip_decode]; + } + return image; } @@ -464,6 +475,30 @@ - (UIImage *)tip_orientationAdjustedImage return image; } +- (UIImage *)tip_imageByUpdatingScale:(CGFloat)scale +{ + if (scale == self.scale) { + return self; + } + + CGImageRef cgImageRef = self.CGImage; + if (cgImageRef) { + return [[UIImage alloc] initWithCGImage:cgImageRef + scale:scale + orientation:self.imageOrientation]; + } + + CGRect imageRect = CGRectZero; + imageRect.size = TIPDimensionsToSizeScaled(self.tip_dimensions, scale); + + return [self tip_imageWithRenderFormatting:^(id _Nonnull format) { + format.scale = scale; + format.renderSize = imageRect.size; + } render:^(UIImage * _Nullable sourceImage, CGContextRef _Nonnull ctx) { + [sourceImage drawInRect:imageRect]; + }]; +} + - (nullable UIImage *)tip_CGImageBackedImageAndReturnError:(out NSError * __autoreleasing __nullable * __nullable)error { __block NSError *outError = nil; @@ -508,8 +543,8 @@ - (nullable UIImage *)tip_CGImageBackedImageAndReturnError:(out NSError * __auto TIPDeferRelease(cgImage); if (cgImage) { image = [UIImage imageWithCGImage:cgImage - scale:[UIScreen mainScreen].scale - orientation:UIImageOrientationUp]; + scale:self.scale + orientation:self.imageOrientation]; } }); @@ -695,6 +730,36 @@ - (nullable UIImage *)tip_imageWithBlurWithRadius:(CGFloat)blurRadius #pragma mark Decode Methods ++ (nullable UIImage *)tip_thumbnailImageWithFileURL:(NSURL *)fileURL + thumbnailMaximumDimension:(CGFloat)thumbnailMaximumDimension +{ + if (!fileURL.isFileURL) { + return nil; + } + + NSDictionary *options = @{ + (id)kCGImageSourceShouldCache : (id)kCFBooleanFalse, + (id)kCGImageSourceThumbnailMaxPixelSize : @(thumbnailMaximumDimension), + (id)kCGImageSourceCreateThumbnailFromImageAlways : (id)kCFBooleanTrue, + (id)kCGImageSourceShouldAllowFloat : (id)kCFBooleanTrue, + (id)kCGImageSourceCreateThumbnailWithTransform : (id)kCFBooleanTrue, + }; + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)fileURL, (CFDictionaryRef)options); + if (!imageSource) { + return nil; + } + + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (CFDictionaryRef)options); + CFRelease(imageSource); + if (!imageRef) { + return nil; + } + + UIImage *image = [UIImage imageWithCGImage:imageRef scale:(CGFloat)1.f orientation:UIImageOrientationUp]; + CFRelease(imageRef); + return image; +} + + (nullable UIImage *)tip_imageWithAnimatedImageFile:(NSString *)filePath durations:(out NSArray * __autoreleasing __nullable * __nullable)durationsOut loopCount:(out NSUInteger * __nullable)loopCountOut diff --git a/TwitterImagePipelineTests/TIPImagePipelineFetchingTests.m b/TwitterImagePipelineTests/TIPImagePipelineFetchingTests.m new file mode 100644 index 0000000..f390f1f --- /dev/null +++ b/TwitterImagePipelineTests/TIPImagePipelineFetchingTests.m @@ -0,0 +1,365 @@ +// +// TIPImagePipelineTests.m +// TwitterImagePipeline +// +// Created on 4/27/15. +// Copyright (c) 2015 Twitter. All rights reserved. +// + +#import "TIP_Project.h" +#import "TIPImageDiskCache.h" +#import "TIPImageMemoryCache.h" +#import "TIPImagePipeline+Project.h" +#import "TIPImageRenderedCache.h" +#import "TIPTestImageFetchDownloadInternalWithStubbing.h" +#import "TIPTests.h" +#import "TIPTestsSharedUtils.h" + +typedef struct _TIPImageFetchTestStruct { + __unsafe_unretained NSString *type; + BOOL progressiveSource; + BOOL isProgressive; + BOOL isAnimated; + uint64_t bps; +} TIPImageFetchTestStruct; + +@interface TIPImagePipelineFetchingBaseTests : TIPImagePipelineBaseTests +- (void)runFetching:(TIPImageFetchTestStruct)imageStruct; // execute fetching test +@end + +@interface TIPImagePipelineFetchingPNGTests : TIPImagePipelineFetchingBaseTests +@end + +@interface TIPImagePipelineFetchingJPEGTests : TIPImagePipelineFetchingBaseTests +@end + +@interface TIPImagePipelineFetchingJPEG2000Tests : TIPImagePipelineFetchingBaseTests +@end + +@interface TIPImagePipelineFetchingGIFTests : TIPImagePipelineFetchingBaseTests +@end + +@implementation TIPImagePipelineFetchingBaseTests + +- (void)_validateFetchOperation:(TIPImageFetchOperation *)op + context:(TIPImagePipelineTestContext *)context + source:(TIPImageLoadSource)source + state:(TIPImageFetchOperationState)state +{ + const BOOL progressiveOn = context.shouldSupportProgressiveLoading; + const BOOL animatedOn = context.shouldSupportAnimatedLoading; + const BOOL shouldReachFinal = (TIPImageFetchOperationStateSucceeded == state); + BOOL metricsWillBeGathered = NO; + if (tip_available_ios_10) { + if ([[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider isKindOfClass:[TIPTestImageFetchDownloadProviderInternalWithStubbing class]]) { + metricsWillBeGathered = YES; + } + } + + NSString *type = [(TIPImagePipelineTestFetchRequest*)op.request imageType]; + XCTAssertEqual(context.didStart, YES, @"imageType == %@", type); + if (shouldReachFinal) { + XCTAssertNotNil(context.finalImageContainer, @"imageType == %@", type); + XCTAssertNil(context.finalError, @"imageType == %@", type); + XCTAssertEqual(context.finalSource, source, @"imageType == %@", type); + XCTAssertEqualObjects(context.finalImageContainer, op.finalResult.imageContainer, @"imageType == %@", type); + XCTAssertEqual(context.finalSource, op.finalResult.imageSource, @"imageType == %@", type); + for (TIPImageLoadSource expectedSource = TIPImageLoadSourceMemoryCache; expectedSource <= source; expectedSource++) { + if (expectedSource == TIPImageLoadSourceNetworkResumed) { + expectedSource++; + } + + if (expectedSource == TIPImageLoadSourceNetwork) { + // be less rigorous about network loading + XCTAssertTrue([context.hitLoadSources containsObject:@(TIPImageLoadSourceNetworkResumed)] || [context.hitLoadSources containsObject:@(TIPImageLoadSourceNetwork)], @"Missing %@ or %@ in %@", @(TIPImageLoadSourceNetwork), @(TIPImageLoadSourceNetworkResumed), context.hitLoadSources); + } else { + // if the source is memory, could be sync load which won't set the "hitLoadSources" which is totally fine + if (expectedSource == TIPImageLoadSourceMemoryCache && source == TIPImageLoadSourceMemoryCache && context.hitLoadSources) { + XCTAssertTrue([context.hitLoadSources containsObject:@(expectedSource)], @"Missing %@ in %@", @(expectedSource), context.hitLoadSources); + } + } + } + if (source == TIPImageLoadSourceNetwork) { + TIPImageFetchMetrics *metrics = op.metrics; + XCTAssertNotNil(metrics); + if (metrics) { + XCTAssertGreaterThan(metrics.totalDuration, 0.0); + XCTAssertFalse(metrics.wasCancelled); + + TIPImageFetchMetricInfo *info = [metrics metricInfoForSource:source]; + XCTAssertNotNil(info); + if (info) { + XCTAssertEqual(info.source, source); + XCTAssertEqual(info.result, TIPImageFetchLoadResultHitFinal); + XCTAssertFalse(info.wasCancelled); + XCTAssertGreaterThan(info.loadDuration, 0.0); + if (metricsWillBeGathered) { + XCTAssertNotNil(info.networkMetrics); + } + XCTAssertGreaterThan(info.totalNetworkLoadDuration, 0.0); + XCTAssertGreaterThan(info.networkImageSizeInBytes, (NSUInteger)0); + XCTAssertEqualObjects(info.networkImageType, type); + XCTAssertTrue(!CGSizeEqualToSize(CGSizeZero, info.networkImageDimensions)); + XCTAssertGreaterThan(info.networkImagePixelsPerByte, 0.0f); + } + } + } + } else { + XCTAssertNil(context.finalImageContainer, @"imageType == %@", type); + XCTAssertNotNil(context.finalError, @"imageType == %@", type); + XCTAssertNil(op.finalResult.imageContainer, @"imageType == %@", type); + XCTAssertEqualObjects(context.finalError, op.error, @"imageType == %@", type); + XCTAssertEqualObjects(context.finalError.domain, TIPImageFetchErrorDomain, @"imageType == %@", type); + } + if (progressiveOn && (TIPImageLoadSourceNetwork == source || TIPImageLoadSourceNetworkResumed == source) && [[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsProgressiveLoading:type]) { + XCTAssertGreaterThan(context.progressiveProgressCount, (NSUInteger)0, @"imageType == %@", type); + } else { + XCTAssertEqual(context.progressiveProgressCount, (NSUInteger)0, @"imageType == %@", type); + } + if (animatedOn && TIPImageLoadSourceNetwork == source && [[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsAnimation:type] && context.expectedFrameCount > 1) { + // only iOS 11 supports progressive animation loading due to a crashing bug in iOS 10 + if (tip_available_ios_11) { + XCTAssertGreaterThan(context.firstAnimatedFrameProgress, (float)0.0f); + XCTAssertLessThan(context.firstAnimatedFrameProgress, (float)1.0f); + } else { + XCTAssertEqualWithAccuracy(context.firstAnimatedFrameProgress, (float)0.0f, (float)0.001f); + } + } + if (animatedOn) { + if (context.finalImageContainer) { + XCTAssertEqual(context.finalImageContainer.frameCount, context.expectedFrameCount); + if (context.expectedFrameCount > 1) { + XCTAssertTrue(context.finalImageContainer.isAnimated); + XCTAssertEqual(context.finalImageContainer.frameDurations.count, context.expectedFrameCount); + for (NSUInteger i = 0; i < context.finalImageContainer.frameCount; i++) { + XCTAssertEqualWithAccuracy([context.finalImageContainer frameDurationAtIndex:i], kFireworksAnimationDurations[i], 0.005); + } + } else { + XCTAssertFalse(context.finalImageContainer.isAnimated); + } + } + } else { + if (context.finalImageContainer) { + XCTAssertEqual(context.finalImageContainer.frameCount, (NSUInteger)1); + } + } + XCTAssertEqual(op.state, state, @"imageType == %@", type); +} + +- (void)runFetching:(TIPImageFetchTestStruct)imageStruct +{ + @autoreleasepool { + BOOL progressive = imageStruct.isProgressive; + BOOL animated = imageStruct.isAnimated; + + TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; + request.imageType = imageStruct.type; + request.progressiveSource = imageStruct.progressiveSource; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + request.targetDimensions = ([imageStruct.type isEqualToString:TIPImageTypeGIF]) ? kFireworksImageDimensions : kCarnivalImageDimensions; + request.targetContentMode = UIViewContentModeScaleAspectFit; + + TIPImageFetchOperation *op = nil; + TIPImagePipelineTestContext *context = nil; + + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:imageStruct.bps * 10 resumable:YES]; // start fast + [[TIPImagePipelineBaseTests sharedPipeline] clearDiskCache]; + [[TIPImagePipelineBaseTests sharedPipeline] clearMemoryCaches]; + + // Network Load + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; + + // NSLog(@"First image: %fs, Last image: %fs", op.metrics.firstImageLoadDuration, op.metrics.totalDuration); + + // Memory Cache Load + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceMemoryCache state:TIPImageFetchOperationStateSucceeded]; + + // Rendered Cache Load + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceMemoryCache state:TIPImageFetchOperationStateSucceeded]; + + // Disk Cache Load + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceDiskCache state:TIPImageFetchOperationStateSucceeded]; + + + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:imageStruct.bps resumable:YES]; // slow it down + + + // Network Load Cancelled + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].diskCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + context.cancelPoint = 0.2f; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateCancelled]; + + + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:imageStruct.bps * 10 resumable:YES]; // speed it up + + + // Network Load Resume + float progress = op.progress; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetworkResumed state:TIPImageFetchOperationStateSucceeded]; + XCTAssertGreaterThanOrEqual(context.firstProgress, progress); + XCTAssertFalse(context.progressWasReset); + + + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:imageStruct.bps resumable:NO]; // slow it down + + + // Network Load Cancelled + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].diskCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + context.cancelPoint = 0.2f; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateCancelled]; + + // Network Load Reset + progress = op.progress; + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; + XCTAssertEqual(context.firstProgress, progress); + XCTAssertTrue(context.progressWasReset); + } +} + +@end + +@implementation TIPImagePipelineFetchingPNGTests + +- (void)testFetchingPNG +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypePNG, NO, NO, NO, 3 * kMegaBits }; + [self runFetching:imageStruct]; +} + +@end + +@implementation TIPImagePipelineFetchingJPEGTests + +- (void)testFetchingJPEG +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, NO, NO, NO, 2 * kMegaBits }; + [self runFetching:imageStruct]; +} + +- (void)testFetchingPJPEG_notProgressive +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, YES, NO, NO, 1 * kMegaBits }; + [self runFetching:imageStruct]; +} + +- (void)testFetchingPJPEG_isProgressive +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, YES, YES, NO, 1 * kMegaBits }; + [self runFetching:imageStruct]; +} + +@end + +@implementation TIPImagePipelineFetchingJPEG2000Tests + +- (void)testFetchingJPEG2000 +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG2000, YES, NO, NO, 256 * kKiloBits }; + if (@available(iOS 11.1, tvOS 11.1, macOS 10.13.1, watchOS 4.1, *)) { + NSLog(@"iOS 11.1 regressed JPEG2000 so that the image cannot be parsed until fully downloading thus breaking most expectations TIP unit tests have. Radars have been filed but please file another radar against Apple if you care about JPEG2000 support."); + } else { + [self runFetching:imageStruct]; + } +} + +- (void)testFetchingPJPEG2000 +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG2000, YES, YES, NO, 256 * kKiloBits }; + if ([[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsProgressiveLoading:imageStruct.type]) { + [self runFetching:imageStruct]; + } else { + NSLog(@"Skipping unit test"); + } +} + +@end + +@implementation TIPImagePipelineFetchingGIFTests + +- (void)testFetchingGIF +{ + TIPImageFetchTestStruct imageStruct = { TIPImageTypeGIF, NO, NO, YES, 160 * kKiloBits }; + [self runFetching:imageStruct]; +} + +- (void)testFetchingSingleFrameGIF +{ + NSBundle *thisBundle = TIPTestsResourceBundle(); + NSString *singleFrameGIFPath = [thisBundle pathForResource:@"single_frame" ofType:@"gif"]; + + @autoreleasepool { + TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; + request.imageType = TIPImageTypeGIF; + request.cannedImageFilePath = singleFrameGIFPath; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + request.targetDimensions = CGSizeMake(360, 200); + request.targetContentMode = UIViewContentModeScaleAspectFit; + + BOOL progressive = NO; + BOOL animated = YES; + + TIPImageFetchOperation *op = nil; + TIPImagePipelineTestContext *context = nil; + + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:64 * kKiloBits resumable:YES]; + + // Network Load + context = [[TIPImagePipelineTestContext alloc] init]; + context.shouldSupportProgressiveLoading = progressive; + context.shouldSupportAnimatedLoading = animated; + context.expectedFrameCount = 1; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; + } +} + +@end diff --git a/TwitterImagePipelineTests/TIPImagePipelineTests.m b/TwitterImagePipelineTests/TIPImagePipelineTests.m index b3c61ce..f20692d 100644 --- a/TwitterImagePipelineTests/TIPImagePipelineTests.m +++ b/TwitterImagePipelineTests/TIPImagePipelineTests.m @@ -6,49 +6,13 @@ // Copyright (c) 2015 Twitter. All rights reserved. // -#import -#import - -#import "TIP_Project.h" #import "TIPGlobalConfiguration+Project.h" #import "TIPImageDiskCache.h" -#import "TIPImageFetchOperation+Project.h" #import "TIPImageMemoryCache.h" #import "TIPImagePipeline+Project.h" #import "TIPImageRenderedCache.h" - -#import "TIPTestImageFetchDownloadInternalWithStubbing.h" - #import "TIPTests.h" - -@import MobileCoreServices; - -static const uint64_t kKiloBits = 1024 * 8; -static const uint64_t kMegaBits = 1024 * kKiloBits; -static const CGSize kCarnivalImageDimensions = { (CGFloat)1880.f, (CGFloat)1253.f }; -static const CGSize kFireworksImageDimensions = { (CGFloat)480.f, (CGFloat)320.f }; -static const NSUInteger kFireworksFrameCount = 10; -static const NSTimeInterval kFireworksAnimationDurations [10] = -{ - .1f, - .15f, - .2f, - .25f, - .3f, - .35f, - .4f, - .45f, - .5f, - .55f, -}; - -typedef struct _TIPImageFetchTestStruct { - __unsafe_unretained NSString *type; - BOOL progressiveSource; - BOOL isProgressive; - BOOL isAnimated; - uint64_t bps; -} TIPImageFetchTestStruct; +#import "TIPTestsSharedUtils.h" @interface TestImageStoreRequest : NSObject @property (nonatomic) NSURL *imageURL; @@ -58,151 +22,91 @@ @interface TestImageStoreRequest : NSObject @implementation TestImageStoreRequest @end -@interface TIPImagePipelineTestFetchRequest : NSObject -@property (nonatomic) NSURL *imageURL; -@property (nonatomic, copy) NSString *imageIdentifier; -@property (nonatomic, copy) TIPImageFetchHydrationBlock imageRequestHydrationBlock; -@property (nonatomic) CGSize targetDimensions; -@property (nonatomic) UIViewContentMode targetContentMode; -@property (nonatomic) NSTimeInterval timeToLive; -@property (nonatomic) TIPImageFetchOptions options; -@property (nonatomic) TIPImageFetchLoadingSources loadingSources; -@property (nonatomic) id jp2ProgressiveLoadingPolicy; -@property (nonatomic) id jpegProgressiveLoadingPolicy; - -@property (nonatomic, copy) NSString *imageType; -@property (nonatomic) BOOL progressiveSource; -@property (nonatomic, copy) NSString *cannedImageFilePath; +@interface TIPImagePipelineTests_Base : TIPImagePipelineBaseTests +- (void)runFillingTheCaches:(TIPImagePipeline *)pipeline bps:(uint64_t)bps testCacheHits:(BOOL)testCacheHits; @end -@interface TIPImagePipelineTestContext : NSObject - -// Populated to configure behavior -@property (nonatomic) BOOL shouldCancelOnPreview; -@property (nonatomic) BOOL shouldSupportProgressiveLoading; -@property (nonatomic) BOOL shouldSupportAnimatedLoading; -@property (nonatomic) float cancelPoint; -@property (nonatomic, weak) TIPImagePipelineTestContext *otherContext; -@property (nonatomic) BOOL shouldCancelOnOtherContextFirstProgress; -@property (nonatomic) NSUInteger expectedFrameCount; - -// Populated by delegate -@property (nonatomic) BOOL didStart; -@property (nonatomic) BOOL didProvidePreviewCheck; -@property (nonatomic) BOOL didMakeProgressiveCheck; -@property (nonatomic) float firstProgress; -@property (nonatomic) float firstAnimatedFrameProgress; -@property (nonatomic) id associatedDownloadContext; -@property (nonatomic) NSUInteger progressiveProgressCount; -@property (nonatomic) NSUInteger normalProgressCount; -@property (nonatomic) TIPImageContainer *finalImageContainer; -@property (nonatomic) NSError *finalError; -@property (nonatomic) TIPImageLoadSource finalSource; -@property (nonatomic, copy) NSArray *hitLoadSources; - +@interface TIPImagePipelineTests_One : TIPImagePipelineTests_Base @end -@interface TIPImagePipeline (Undeprecated) -- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context delegate:(nullable id)delegate; -- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context completion:(nullable TIPImagePipelineFetchCompletionBlock)completion; +@interface TIPImagePipelineTests_Two : TIPImagePipelineTests_Base @end -@interface TIPImagePipelineTests : XCTestCase +@interface TIPImagePipelineTests_Three : TIPImagePipelineTests_Base @end -static TIPImagePipeline *sPipeline = nil; - -@implementation TIPImagePipelineTests +@implementation TIPImagePipelineTests_Base -+ (NSString *)pathForImageOfType:(NSString *)type progressive:(BOOL)progressive +- (void)runFillingTheCaches:(TIPImagePipeline *)pipeline bps:(uint64_t)bps testCacheHits:(BOOL)testCacheHits { - NSString *imagePath = nil; - NSBundle *thisBundle = TIPTestsResourceBundle(); + id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; - if ([type isEqualToString:TIPImageTypeGIF]) { - imagePath = [thisBundle pathForResource:@"fireworks" ofType:@"gif"]; - } else { - NSString *extension = nil; - - if ([type isEqualToString:TIPImageTypeJPEG]) { - extension = (progressive) ? @"pjpg" : @"jpg"; - } else if ([type isEqualToString:TIPImageTypeJPEG2000]) { - extension = @"jp2"; - } else if ([type isEqualToString:TIPImageTypePNG]) { - extension = @"png"; - } + NSMutableArray *URLs = [NSMutableArray array]; + for (NSUInteger i = 0; i < 10; i++) { + [URLs addObject:[TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]]; + } - if (extension) { - imagePath = [thisBundle pathForResource:@"carnival" ofType:extension]; + // First pass, load em up + // Second pass (if testCacheHits), reload since older version will have been purged by full cache + const NSUInteger numberOfRuns = (testCacheHits) ? 2 : 1; + for (NSUInteger i = 0; i < numberOfRuns; i++) { + for (NSURL *URL in URLs) { + @autoreleasepool { + TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; + request.imageType = TIPImageTypeJPEG; + request.progressiveSource = YES; + request.imageURL = URL; + request.targetDimensions = kCarnivalImageDimensions; + request.targetContentMode = UIViewContentModeScaleToFill; + TIPImagePipelineTestContext *context = [[TIPImagePipelineTestContext alloc] init]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:bps resumable:YES]; + TIPImageFetchOperation *op = [pipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [provider removeDownloadStubForRequestURL:request.imageURL]; + XCTAssertEqual(op.state, TIPImageFetchOperationStateSucceeded); + XCTAssertEqual(context.finalSource, TIPImageLoadSourceNetwork); + } } } - return imagePath; -} - -+ (NSURL *)dummyURLWithPath:(NSString *)path -{ - if (!path) { - path = @""; - } - if (![path hasPrefix:@"/"]) { - path = [@"/" stringByAppendingString:path]; + // visit in reverse order + NSUInteger memMatches = 0; + NSUInteger diskMatches = 0; + for (NSURL *URL in URLs.reverseObjectEnumerator) { + TIPImageLoadSource source = TIPImageLoadSourceUnknown; + @autoreleasepool { + TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; + request.imageType = TIPImageTypeJPEG; + request.progressiveSource = YES; + request.imageURL = URL; + request.targetDimensions = kCarnivalImageDimensions; + request.targetContentMode = UIViewContentModeScaleToFill; + TIPImagePipelineTestContext *context = [[TIPImagePipelineTestContext alloc] init]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:bps resumable:YES]; + TIPImageFetchOperation *op = [pipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; + [op waitUntilFinishedWithoutBlockingRunLoop]; + [provider removeDownloadStubForRequestURL:request.imageURL]; + XCTAssertEqual(op.state, TIPImageFetchOperationStateSucceeded); + source = op.finalResult.imageSource; + if (source == TIPImageLoadSourceMemoryCache) { + memMatches++; + } else if (source == TIPImageLoadSourceDiskCache) { + diskMatches++; + } else { + break; + } + } } - return [NSURL URLWithString:[NSString stringWithFormat:@"http://www.dummy.com%@", path]]; -} -+ (void)setUp -{ - TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; - TIPSetDebugSTOPOnAssertEnabled(NO); - TIPSetShouldAssertDuringPipelineRegistation(NO); - sPipeline = [[TIPImagePipeline alloc] initWithIdentifier:NSStringFromClass(self)]; - globalConfig.imageFetchDownloadProvider = [[TIPTestsImageFetchDownloadProviderOverrideClass() alloc] init]; - globalConfig.maxConcurrentImagePipelineDownloadCount = 4; - globalConfig.maxBytesForAllRenderedCaches = 12 * 1024 * 1024; - globalConfig.maxBytesForAllMemoryCaches = 36 * 1024 * 1024; - globalConfig.maxBytesForAllDiskCaches = 16 * 1024 * 1024; - globalConfig.maxRatioSizeOfCacheEntry = 0; -} - -+ (void)tearDown -{ - TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; - TIPSetDebugSTOPOnAssertEnabled(YES); - TIPSetShouldAssertDuringPipelineRegistation(YES); - [sPipeline.renderedCache clearAllImages:NULL]; - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - globalConfig.imageFetchDownloadProvider = nil; - globalConfig.maxBytesForAllRenderedCaches = -1; - globalConfig.maxBytesForAllMemoryCaches = -1; - globalConfig.maxBytesForAllDiskCaches = -1; - globalConfig.maxConcurrentImagePipelineDownloadCount = TIPMaxConcurrentImagePipelineDownloadCountDefault; - globalConfig.maxRatioSizeOfCacheEntry = -1; - - sPipeline = nil; + if (testCacheHits) { + XCTAssertGreaterThan(memMatches, (NSUInteger)0); + XCTAssertGreaterThan(diskMatches, (NSUInteger)0); + } } -- (void)tearDown -{ - [sPipeline.renderedCache clearAllImages:NULL]; - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - - id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; - [provider removeAllDownloadStubs]; +@end - // Flush ALL pipelines - __block BOOL didInspect = NO; - [[TIPGlobalConfiguration sharedInstance] inspect:^(NSDictionary *results) { - didInspect = YES; - }]; - while (!didInspect) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - } - - [super tearDown]; -} +@implementation TIPImagePipelineTests_One - (void)testImagePipelineConstruction { @@ -233,434 +137,12 @@ - (void)testImagePipelineConstruction } @autoreleasepool { - pipeline = [[TIPImagePipeline alloc] initWithIdentifier:sPipeline.identifier]; + pipeline = [[TIPImagePipeline alloc] initWithIdentifier:[TIPImagePipelineBaseTests sharedPipeline].identifier]; XCTAssertNil(pipeline); pipeline = nil; } } -- (void)_validateFetchOperation:(TIPImageFetchOperation *)op - context:(TIPImagePipelineTestContext *)context - source:(TIPImageLoadSource)source - state:(TIPImageFetchOperationState)state -{ - const BOOL progressiveOn = context.shouldSupportProgressiveLoading; - const BOOL animatedOn = context.shouldSupportAnimatedLoading; - const BOOL shouldReachFinal = (TIPImageFetchOperationStateSucceeded == state); - BOOL metricsWillBeGathered = NO; - if (tip_available_ios_10) { - if ([[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider isKindOfClass:[TIPTestImageFetchDownloadProviderInternalWithStubbing class]]) { - metricsWillBeGathered = YES; - } - } - - NSString *type = [(TIPImagePipelineTestFetchRequest*)op.request imageType]; - XCTAssertEqual(context.didStart, YES, @"imageType == %@", type); - if (shouldReachFinal) { - XCTAssertNotNil(context.finalImageContainer, @"imageType == %@", type); - XCTAssertNil(context.finalError, @"imageType == %@", type); - XCTAssertEqual(context.finalSource, source, @"imageType == %@", type); - XCTAssertEqualObjects(context.finalImageContainer, op.finalResult.imageContainer, @"imageType == %@", type); - XCTAssertEqual(context.finalSource, op.finalResult.imageSource, @"imageType == %@", type); - for (TIPImageLoadSource expectedSource = TIPImageLoadSourceMemoryCache; expectedSource <= source; expectedSource++) { - if (expectedSource == TIPImageLoadSourceNetworkResumed) { - expectedSource++; - } - - if (expectedSource == TIPImageLoadSourceNetwork) { - // be less rigorous about network loading - XCTAssertTrue([context.hitLoadSources containsObject:@(TIPImageLoadSourceNetworkResumed)] || [context.hitLoadSources containsObject:@(TIPImageLoadSourceNetwork)], @"Missing %@ or %@ in %@", @(TIPImageLoadSourceNetwork), @(TIPImageLoadSourceNetworkResumed), context.hitLoadSources); - } else { - // if the source is memory, could be sync load which won't set the "hitLoadSources" which is totally fine - if (expectedSource == TIPImageLoadSourceMemoryCache && source == TIPImageLoadSourceMemoryCache && context.hitLoadSources) { - XCTAssertTrue([context.hitLoadSources containsObject:@(expectedSource)], @"Missing %@ in %@", @(expectedSource), context.hitLoadSources); - } - } - } - if (source == TIPImageLoadSourceNetwork) { - TIPImageFetchMetrics *metrics = op.metrics; - XCTAssertNotNil(metrics); - if (metrics) { - XCTAssertGreaterThan(metrics.totalDuration, 0.0); - XCTAssertFalse(metrics.wasCancelled); - - TIPImageFetchMetricInfo *info = [metrics metricInfoForSource:source]; - XCTAssertNotNil(info); - if (info) { - XCTAssertEqual(info.source, source); - XCTAssertEqual(info.result, TIPImageFetchLoadResultHitFinal); - XCTAssertFalse(info.wasCancelled); - XCTAssertGreaterThan(info.loadDuration, 0.0); - if (metricsWillBeGathered) { - XCTAssertNotNil(info.networkMetrics); - } - XCTAssertGreaterThan(info.totalNetworkLoadDuration, 0.0); - XCTAssertGreaterThan(info.networkImageSizeInBytes, (NSUInteger)0); - XCTAssertEqualObjects(info.networkImageType, type); - XCTAssertTrue(!CGSizeEqualToSize(CGSizeZero, info.networkImageDimensions)); - XCTAssertGreaterThan(info.networkImagePixelsPerByte, 0.0f); - } - } - } - } else { - XCTAssertNil(context.finalImageContainer, @"imageType == %@", type); - XCTAssertNotNil(context.finalError, @"imageType == %@", type); - XCTAssertNil(op.finalResult.imageContainer, @"imageType == %@", type); - XCTAssertEqualObjects(context.finalError, op.error, @"imageType == %@", type); - XCTAssertEqualObjects(context.finalError.domain, TIPImageFetchErrorDomain, @"imageType == %@", type); - } - if (progressiveOn && (TIPImageLoadSourceNetwork == source || TIPImageLoadSourceNetworkResumed == source) && [[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsProgressiveLoading:type]) { - XCTAssertGreaterThan(context.progressiveProgressCount, (NSUInteger)0, @"imageType == %@", type); - } else { - XCTAssertEqual(context.progressiveProgressCount, (NSUInteger)0, @"imageType == %@", type); - } - if (animatedOn && TIPImageLoadSourceNetwork == source && [[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsAnimation:type] && context.expectedFrameCount > 1) { - // only iOS 11 supports progressive animation loading due to a crashing bug in iOS 10 - if (tip_available_ios_11) { - XCTAssertGreaterThan(context.firstAnimatedFrameProgress, (float)0.0f); - XCTAssertLessThan(context.firstAnimatedFrameProgress, (float)1.0f); - } else { - XCTAssertEqualWithAccuracy(context.firstAnimatedFrameProgress, (float)0.0f, (float)0.001f); - } - } - if (animatedOn) { - if (context.finalImageContainer) { - XCTAssertEqual(context.finalImageContainer.frameCount, context.expectedFrameCount); - if (context.expectedFrameCount > 1) { - XCTAssertTrue(context.finalImageContainer.isAnimated); - XCTAssertEqual(context.finalImageContainer.frameDurations.count, context.expectedFrameCount); - for (NSUInteger i = 0; i < context.finalImageContainer.frameCount; i++) { - XCTAssertEqualWithAccuracy([context.finalImageContainer frameDurationAtIndex:i], kFireworksAnimationDurations[i], 0.005); - } - } else { - XCTAssertFalse(context.finalImageContainer.isAnimated); - } - } - } else { - if (context.finalImageContainer) { - XCTAssertEqual(context.finalImageContainer.frameCount, (NSUInteger)1); - } - } - XCTAssertEqual(op.state, state, @"imageType == %@", type); -} - -- (void)_stubRequest:(TIPImagePipelineTestFetchRequest *)request bitrate:(uint64_t)bitrate resumable:(BOOL)resumable -{ - NSData *data = [NSData dataWithContentsOfFile:request.cannedImageFilePath options:NSDataReadingMappedIfSafe error:NULL]; - NSString *MIMEType = (NSString *)CFBridgingRelease(UTTypeIsDeclared((__bridge CFStringRef)request.imageType) ? UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)request.imageType, kUTTagClassMIMEType) : nil); - id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; - [provider addDownloadStubForRequestURL:request.imageURL responseData:data responseMIMEType:MIMEType shouldSupportResuming:resumable suggestedBitrate:bitrate]; -} - -- (void)_runFetching:(TIPImageFetchTestStruct)imageStruct -{ - @autoreleasepool { - BOOL progressive = imageStruct.isProgressive; - BOOL animated = imageStruct.isAnimated; - - TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; - request.imageType = imageStruct.type; - request.progressiveSource = imageStruct.progressiveSource; - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; - request.targetDimensions = ([imageStruct.type isEqualToString:TIPImageTypeGIF]) ? kFireworksImageDimensions : kCarnivalImageDimensions; - request.targetContentMode = UIViewContentModeScaleAspectFit; - - TIPImageFetchOperation *op = nil; - TIPImagePipelineTestContext *context = nil; - - [self _stubRequest:request bitrate:imageStruct.bps * 10 resumable:YES]; // start fast - [sPipeline clearDiskCache]; - [sPipeline clearMemoryCaches]; - - // Network Load - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; - - // NSLog(@"First image: %fs, Last image: %fs", op.metrics.firstImageLoadDuration, op.metrics.totalDuration); - - // Memory Cache Load - [sPipeline.renderedCache clearAllImages:NULL]; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceMemoryCache state:TIPImageFetchOperationStateSucceeded]; - - // Rendered Cache Load - [sPipeline.memoryCache clearAllImages:NULL]; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceMemoryCache state:TIPImageFetchOperationStateSucceeded]; - - // Disk Cache Load - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceDiskCache state:TIPImageFetchOperationStateSucceeded]; - - - [self _stubRequest:request bitrate:imageStruct.bps resumable:YES]; // slow it down - - - // Network Load Cancelled - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - context.cancelPoint = 0.2f; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateCancelled]; - - - [self _stubRequest:request bitrate:imageStruct.bps * 10 resumable:YES]; // speed it up - - - // Network Load Resume - float progress = op.progress; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetworkResumed state:TIPImageFetchOperationStateSucceeded]; - XCTAssertGreaterThanOrEqual(context.firstProgress, progress); - - - [self _stubRequest:request bitrate:imageStruct.bps resumable:NO]; // slow it down - - - // Network Load Cancelled - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - context.cancelPoint = 0.2f; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateCancelled]; - - // Network Load Reset - progress = op.progress; - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; - XCTAssertLessThan(context.firstProgress, progress); - } -} - -- (void)testFetchingPNG -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypePNG, NO, NO, NO, 3 * kMegaBits }; - [self _runFetching:imageStruct]; -} - -- (void)testFetchingJPEG -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, NO, NO, NO, 2 * kMegaBits }; - [self _runFetching:imageStruct]; -} - -- (void)testFetchingPJPEG_notProgressive -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, YES, NO, NO, 1 * kMegaBits }; - [self _runFetching:imageStruct]; -} - -- (void)testFetchingPJPEG_isProgressive -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG, YES, YES, NO, 1 * kMegaBits }; - [self _runFetching:imageStruct]; -} - -- (void)testFetchingJPEG2000 -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG2000, YES, NO, NO, 256 * kKiloBits }; - if (@available(iOS 11.1, tvOS 11.1, macOS 10.13.1, watchOS 4.1, *)) { - NSLog(@"iOS 11.1 regressed JPEG2000 so that the image cannot be parsed until fully downloading thus breaking most expectations TIP unit tests have. Radars have been filed but please file another radar against Apple if you care about JPEG2000 support."); - } else { - [self _runFetching:imageStruct]; - } -} - -- (void)testFetchingPJPEG2000 -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeJPEG2000, YES, YES, NO, 256 * kKiloBits }; - if ([[TIPImageCodecCatalogue sharedInstance] codecWithImageTypeSupportsProgressiveLoading:imageStruct.type]) { - [self _runFetching:imageStruct]; - } else { - NSLog(@"Skipping unit test"); - } -} - -- (void)testFetchingGIF -{ - TIPImageFetchTestStruct imageStruct = { TIPImageTypeGIF, NO, NO, YES, 160 * kKiloBits }; - [self _runFetching:imageStruct]; -} - -- (void)testFetchingSingleFrameGIF -{ - NSBundle *thisBundle = TIPTestsResourceBundle(); - NSString *singleFrameGIFPath = [thisBundle pathForResource:@"single_frame" ofType:@"gif"]; - - @autoreleasepool { - TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; - request.imageType = TIPImageTypeGIF; - request.cannedImageFilePath = singleFrameGIFPath; - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; - request.targetDimensions = CGSizeMake(360, 200); - request.targetContentMode = UIViewContentModeScaleAspectFit; - - BOOL progressive = NO; - BOOL animated = YES; - - TIPImageFetchOperation *op = nil; - TIPImagePipelineTestContext *context = nil; - - [self _stubRequest:request bitrate:64 * kKiloBits resumable:YES]; - - // Network Load - context = [[TIPImagePipelineTestContext alloc] init]; - context.shouldSupportProgressiveLoading = progressive; - context.shouldSupportAnimatedLoading = animated; - context.expectedFrameCount = 1; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [self _validateFetchOperation:op context:context source:TIPImageLoadSourceNetwork state:TIPImageFetchOperationStateSucceeded]; - } -} - -- (void)testFillingTheCaches -{ - [self _runFillingTheCaches:sPipeline bps:1024 * kMegaBits testCacheHits:YES]; -} - -- (void)testFillingMultipeCaches -{ - TIPGlobalConfiguration *config = [TIPGlobalConfiguration sharedInstance]; - - __block SInt64 preDeallocDiskSize; - __block SInt64 preDeallocMemSize; - __block SInt64 preDeallocRendSize; - - __block SInt64 preDeallocPipelineDiskSize; - __block SInt64 preDeallocPipelineMemSize; - __block SInt64 preDeallocPipelineRendSize; - - NSString *tmpPipelineIdentifier = @"temp.pipeline.identifier"; - XCTestExpectation *expectation = [self expectationForNotification:TIPImagePipelineDidTearDownImagePipelineNotification object:nil handler:^BOOL(NSNotification *note) { - return [tmpPipelineIdentifier isEqualToString:note.userInfo[TIPImagePipelineImagePipelineIdentifierNotificationKey]]; - }]; - - @autoreleasepool { - [sPipeline clearMemoryCaches]; - [sPipeline clearDiskCache]; - TIPImagePipeline *temporaryPipeline = [[TIPImagePipeline alloc] initWithIdentifier:tmpPipelineIdentifier]; - - [self _runFillingTheCaches:sPipeline bps:1024 * kMegaBits testCacheHits:NO]; - - TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; - - XCTAssertGreaterThan(sPipeline.renderedCache.manifest.numberOfEntries, (NSUInteger)0); - dispatch_sync(globalConfig.queueForMemoryCaches, ^{ - XCTAssertGreaterThan(sPipeline.memoryCache.manifest.numberOfEntries, (NSUInteger)0); - }); - dispatch_sync(globalConfig.queueForDiskCaches, ^{ - XCTAssertGreaterThan(sPipeline.diskCache.manifest.numberOfEntries, (NSUInteger)0); - }); - - [self _runFillingTheCaches:temporaryPipeline bps:1024 * kMegaBits testCacheHits:NO]; - XCTAssertGreaterThan(temporaryPipeline.renderedCache.manifest.numberOfEntries, (NSUInteger)0); - XCTAssertEqual(sPipeline.renderedCache.manifest.numberOfEntries, (NSUInteger)0); - dispatch_sync(globalConfig.queueForMemoryCaches, ^{ - XCTAssertGreaterThan(temporaryPipeline.memoryCache.manifest.numberOfEntries, (NSUInteger)0); - XCTAssertEqual(sPipeline.memoryCache.manifest.numberOfEntries, (NSUInteger)0); - }); - dispatch_sync(globalConfig.queueForMemoryCaches, ^{ - XCTAssertGreaterThan(temporaryPipeline.diskCache.manifest.numberOfEntries, (NSUInteger)0); - XCTAssertEqual(sPipeline.diskCache.manifest.numberOfEntries, (NSUInteger)0); - }); - - dispatch_sync(globalConfig.queueForDiskCaches, ^{ - preDeallocDiskSize = config.internalTotalBytesForAllDiskCaches; - }); - dispatch_sync(globalConfig.queueForMemoryCaches, ^{ - preDeallocMemSize = config.internalTotalBytesForAllMemoryCaches; - }); - preDeallocRendSize = config.internalTotalBytesForAllRenderedCaches; - - preDeallocPipelineDiskSize = (SInt64)temporaryPipeline.diskCache.totalCost; - preDeallocPipelineMemSize = (SInt64)temporaryPipeline.memoryCache.totalCost; - preDeallocPipelineRendSize = (SInt64)temporaryPipeline.renderedCache.totalCost; - - temporaryPipeline = nil; - } - - NSLog(@"Waiting for %@", TIPImagePipelineDidTearDownImagePipelineNotification); - - // Wait for the pipeline to release - [self waitForExpectationsWithTimeout:120.0 handler:^(NSError *error) { - if (error) { - NSLog(@"%@", error); - } else { - NSLog(@"Received %@", TIPImagePipelineDidTearDownImagePipelineNotification); - } - }]; - expectation = nil; - - __block SInt64 postDeallocDiskSize; - __block SInt64 postDeallocMemSize; - __block SInt64 postDeallocRendSize; - - const NSUInteger cacheSizeCheckMax = 30; - NSUInteger cacheSizeCheck; - for (cacheSizeCheck = 1; cacheSizeCheck <= cacheSizeCheckMax; cacheSizeCheck++) { - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; - - dispatch_sync([TIPGlobalConfiguration sharedInstance].queueForDiskCaches, ^{ - postDeallocDiskSize = config.internalTotalBytesForAllDiskCaches; - }); - dispatch_sync([TIPGlobalConfiguration sharedInstance].queueForMemoryCaches, ^{ - postDeallocMemSize = config.internalTotalBytesForAllMemoryCaches; - }); - postDeallocRendSize = config.internalTotalBytesForAllRenderedCaches; - - if (postDeallocDiskSize == 0 && postDeallocMemSize == 0 && postDeallocRendSize == 0) { - break; - } - } - - if (cacheSizeCheck <= cacheSizeCheckMax) { - NSLog(@"Caches were relieved after %tu seconds", cacheSizeCheck); - } else { - NSLog(@"ERR: Caches were not relieved after %tu seconds", cacheSizeCheck - 1); - } - - XCTAssertEqual(postDeallocDiskSize, preDeallocDiskSize - preDeallocPipelineDiskSize); - XCTAssertEqual(postDeallocMemSize, preDeallocMemSize - preDeallocPipelineMemSize); - XCTAssertEqual(postDeallocRendSize, preDeallocRendSize - preDeallocPipelineRendSize); -} - - (void)testConcurrentManifestLoad { NSArray *(^buildComparablesFromPipeline)(TIPImagePipeline *) = ^(TIPImagePipeline *pipeline) { @@ -696,8 +178,8 @@ - (void)testConcurrentManifestLoad NSMutableArray *stubbedRequestURLs = [NSMutableArray array]; NSOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{}]; for (NSUInteger i = 0; i < 150; i++) { - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; - [self _stubRequest:request bitrate:UINT64_MAX resumable:YES]; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:UINT64_MAX resumable:YES]; [stubbedRequestURLs addObject:request.imageURL]; TIPImageFetchOperation *op = [initialPipeline undeprecatedFetchImageWithRequest:request context:nil delegate:nil]; [blockOp addDependency:op]; @@ -759,98 +241,31 @@ - (void)_safelyOpenPipelineWithIdentifier:(NSString *)identifier executingBlock: [self waitForExpectationsWithTimeout:20 handler:nil]; } -- (void)_runFillingTheCaches:(TIPImagePipeline *)pipeline bps:(uint64_t)bps testCacheHits:(BOOL)testCacheHits -{ - id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; - - NSMutableArray *URLs = [NSMutableArray array]; - for (NSUInteger i = 0; i < 10; i++) { - [URLs addObject:[TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]]; - } - - // First pass, load em up - // Second pass (if testCacheHits), reload since older version will have been purged by full cache - const NSUInteger numberOfRuns = (testCacheHits) ? 2 : 1; - for (NSUInteger i = 0; i < numberOfRuns; i++) { - for (NSURL *URL in URLs) { - @autoreleasepool { - TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; - request.imageType = TIPImageTypeJPEG; - request.progressiveSource = YES; - request.imageURL = URL; - request.targetDimensions = kCarnivalImageDimensions; - request.targetContentMode = UIViewContentModeScaleToFill; - TIPImagePipelineTestContext *context = [[TIPImagePipelineTestContext alloc] init]; - [self _stubRequest:request bitrate:bps resumable:YES]; - TIPImageFetchOperation *op = [pipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [provider removeDownloadStubForRequestURL:request.imageURL]; - XCTAssertEqual(op.state, TIPImageFetchOperationStateSucceeded); - XCTAssertEqual(context.finalSource, TIPImageLoadSourceNetwork); - } - } - } - - // visit in reverse order - NSUInteger memMatches = 0; - NSUInteger diskMatches = 0; - for (NSURL *URL in URLs.reverseObjectEnumerator) { - TIPImageLoadSource source = TIPImageLoadSourceUnknown; - @autoreleasepool { - TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; - request.imageType = TIPImageTypeJPEG; - request.progressiveSource = YES; - request.imageURL = URL; - request.targetDimensions = kCarnivalImageDimensions; - request.targetContentMode = UIViewContentModeScaleToFill; - TIPImagePipelineTestContext *context = [[TIPImagePipelineTestContext alloc] init]; - [self _stubRequest:request bitrate:bps resumable:YES]; - TIPImageFetchOperation *op = [pipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; - [op waitUntilFinishedWithoutBlockingRunLoop]; - [provider removeDownloadStubForRequestURL:request.imageURL]; - XCTAssertEqual(op.state, TIPImageFetchOperationStateSucceeded); - source = op.finalResult.imageSource; - if (source == TIPImageLoadSourceMemoryCache) { - memMatches++; - } else if (source == TIPImageLoadSourceDiskCache) { - diskMatches++; - } else { - break; - } - } - } - - if (testCacheHits) { - XCTAssertGreaterThan(memMatches, (NSUInteger)0); - XCTAssertGreaterThan(diskMatches, (NSUInteger)0); - } -} - - (void)testMergingFetches { TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; request.imageType = TIPImageTypeJPEG; request.progressiveSource = YES; - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; request.targetDimensions = kCarnivalImageDimensions; request.targetContentMode = UIViewContentModeScaleAspectFit; - [self _stubRequest:request bitrate:2 * kMegaBits resumable:YES]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:2 * kMegaBits resumable:YES]; TIPImageFetchOperation *op1 = nil; TIPImageFetchOperation *op2 = nil; TIPImagePipelineTestContext *context1 = nil; TIPImagePipelineTestContext *context2 = nil; - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].diskCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; context1 = [[TIPImagePipelineTestContext alloc] init]; context2 = [[TIPImagePipelineTestContext alloc] init]; context1.otherContext = context2; - op1 = [sPipeline undeprecatedFetchImageWithRequest:request context:context1 delegate:self]; + op1 = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context1 delegate:self]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - op2 = [sPipeline undeprecatedFetchImageWithRequest:request context:context2 delegate:self]; + op2 = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context2 delegate:self]; [op1 waitUntilFinishedWithoutBlockingRunLoop]; [op2 waitUntilFinishedWithoutBlockingRunLoop]; @@ -876,16 +291,16 @@ - (void)testMergingFetches // Cancel original - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].diskCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; context1 = [[TIPImagePipelineTestContext alloc] init]; context1.shouldCancelOnOtherContextFirstProgress = YES; context2 = [[TIPImagePipelineTestContext alloc] init]; context1.otherContext = context2; - op1 = [sPipeline undeprecatedFetchImageWithRequest:request context:context1 delegate:self]; + op1 = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context1 delegate:self]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - op2 = [sPipeline undeprecatedFetchImageWithRequest:request context:context2 delegate:self]; + op2 = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context2 delegate:self]; [op1 waitUntilFinishedWithoutBlockingRunLoop]; [op2 waitUntilFinishedWithoutBlockingRunLoop]; @@ -909,13 +324,13 @@ - (void)testMergingFetches - (void)testCopyingDiskEntry { - [sPipeline clearDiskCache]; - [sPipeline clearMemoryCaches]; + [[TIPImagePipelineBaseTests sharedPipeline] clearDiskCache]; + [[TIPImagePipelineBaseTests sharedPipeline] clearMemoryCaches]; NSString *copyFinishedNotificationName = @"copy_finished"; TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; request.imageType = TIPImageTypeJPEG; request.progressiveSource = NO; @@ -932,22 +347,22 @@ - (void)testCopyingDiskEntry }); }; - [self _stubRequest:request bitrate:1024 * kMegaBits resumable:YES]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:1024 * kMegaBits resumable:YES]; // Attempt with empty caches tempFile = nil; copyError = nil; finisedCopyExpectation = [self expectationForNotification:copyFinishedNotificationName object:nil handler:NULL]; - [sPipeline copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; + [[TIPImagePipelineBaseTests sharedPipeline] copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; [self waitForExpectationsWithTimeout:5.0 handler:NULL]; XCTAssertNil(tempFile); XCTAssertNotNil(copyError); // Fill cache with item - TIPImageFetchOperation *op = [sPipeline operationWithRequest:request context:nil completion:NULL]; - [sPipeline fetchImageWithOperation:op]; + TIPImageFetchOperation *op = [[TIPImagePipelineBaseTests sharedPipeline] operationWithRequest:request context:nil completion:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline] fetchImageWithOperation:op]; [op waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertNotNil(op.finalResult.imageContainer); @@ -956,7 +371,7 @@ - (void)testCopyingDiskEntry tempFile = nil; copyError = nil; finisedCopyExpectation = [self expectationForNotification:copyFinishedNotificationName object:nil handler:NULL]; - [sPipeline copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; + [[TIPImagePipelineBaseTests sharedPipeline] copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; [self waitForExpectationsWithTimeout:5.0 handler:NULL]; XCTAssertNotNil(tempFile); XCTAssertNil(copyError); @@ -966,9 +381,9 @@ - (void)testCopyingDiskEntry tempFile = nil; copyError = nil; - [sPipeline clearDiskCache]; + [[TIPImagePipelineBaseTests sharedPipeline] clearDiskCache]; finisedCopyExpectation = [self expectationForNotification:copyFinishedNotificationName object:nil handler:NULL]; - [sPipeline copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; + [[TIPImagePipelineBaseTests sharedPipeline] copyDiskCacheFileWithIdentifier:request.imageURL.absoluteString completion:completion]; [self waitForExpectationsWithTimeout:5.0 handler:NULL]; XCTAssertNil(tempFile); XCTAssertNotNil(copyError); @@ -1040,7 +455,7 @@ - (void)testCrossPipelineLoad fetchRequest.imageType = TIPImageTypeJPEG; fetchRequest.progressiveSource = NO; - [self _stubRequest:fetchRequest bitrate:0 resumable:YES]; + [TIPImagePipelineTestFetchRequest stubRequest:fetchRequest bitrate:0 resumable:YES]; expectation = [self expectationWithDescription:@"Cross Pipeline Fetch Image 1"]; op = [pipeline2 operationWithRequest:fetchRequest context:nil completion:^(id result, NSError *error) { @@ -1105,8 +520,8 @@ - (void)testRenamedEntry fetchRequest2.progressiveSource = NO; fetchRequest2.loadingSources = TIPImageFetchLoadingSourcesAll & ~(TIPImageFetchLoadingSourceNetwork | TIPImageFetchLoadingSourceNetworkResumed); // no network! - [self _stubRequest:fetchRequest1 bitrate:0 resumable:YES]; - [self _stubRequest:fetchRequest2 bitrate:0 resumable:NO]; // just to ensure we don't hit the network + [TIPImagePipelineTestFetchRequest stubRequest:fetchRequest1 bitrate:0 resumable:YES]; + [TIPImagePipelineTestFetchRequest stubRequest:fetchRequest2 bitrate:0 resumable:NO]; // just to ensure we don't hit the network expectation = [self expectationWithDescription:@"Pipeline Fetch Image 1"]; op = [pipeline operationWithRequest:fetchRequest1 context:nil completion:^(id result, NSError *error) { @@ -1173,21 +588,21 @@ - (void)testInvalidPseudoFilePathFetch TIPImagePipelineTestFetchRequest *request = [[TIPImagePipelineTestFetchRequest alloc] init]; request.imageType = TIPImageTypeJPEG; request.progressiveSource = YES; - request.imageURL = [TIPImagePipelineTests dummyURLWithPath:[NSUUID UUID].UUIDString]; + request.imageURL = [TIPImagePipelineBaseTests dummyURLWithPath:[NSUUID UUID].UUIDString]; request.targetDimensions = kCarnivalImageDimensions; request.targetContentMode = UIViewContentModeScaleAspectFit; request.cannedImageFilePath = [request.cannedImageFilePath stringByAppendingPathExtension:@"dne"]; - [self _stubRequest:request bitrate:1 * kMegaBits resumable:YES]; + [TIPImagePipelineTestFetchRequest stubRequest:request bitrate:1 * kMegaBits resumable:YES]; TIPImageFetchOperation *op = nil; TIPImagePipelineTestContext *context = nil; - [sPipeline.memoryCache clearAllImages:NULL]; - [sPipeline.diskCache clearAllImages:NULL]; - [sPipeline.renderedCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].memoryCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].diskCache clearAllImages:NULL]; + [[TIPImagePipelineBaseTests sharedPipeline].renderedCache clearAllImages:NULL]; context = [[TIPImagePipelineTestContext alloc] init]; - op = [sPipeline undeprecatedFetchImageWithRequest:request context:context delegate:self]; + op = [[TIPImagePipelineBaseTests sharedPipeline] undeprecatedFetchImageWithRequest:request context:context delegate:self]; [op waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertNil(op.finalResult.imageContainer); @@ -1197,149 +612,123 @@ - (void)testInvalidPseudoFilePathFetch (void)metricInfo; } -#pragma mark Delegate +@end -- (void)tip_imageFetchOperationDidStart:(TIPImageFetchOperation *)op -{ - TIPImagePipelineTestContext *context = op.context; - context.didStart = YES; -} +@implementation TIPImagePipelineTests_Two -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op willAttemptToLoadFromSource:(TIPImageLoadSource)source +- (void)testFillingMultipleCaches { - TIPImagePipelineTestContext *context = op.context; - NSArray *existing = context.hitLoadSources ?: @[]; - context.hitLoadSources = [existing arrayByAddingObject:@(source)]; -} + TIPGlobalConfiguration *config = [TIPGlobalConfiguration sharedInstance]; -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadPreviewImage:(id)previewResult completion:(TIPImageFetchDidLoadPreviewCallback)completion -{ - TIPImagePipelineTestContext *context = op.context; - context.didProvidePreviewCheck = YES; + __block SInt64 preDeallocDiskSize; + __block SInt64 preDeallocMemSize; + __block SInt64 preDeallocRendSize; - completion(context.shouldCancelOnPreview ? TIPImageFetchPreviewLoadedBehaviorStopLoading : TIPImageFetchPreviewLoadedBehaviorContinueLoading); -} + __block SInt64 preDeallocPipelineDiskSize; + __block SInt64 preDeallocPipelineMemSize; + __block SInt64 preDeallocPipelineRendSize; -- (BOOL)tip_imageFetchOperation:(TIPImageFetchOperation *)op shouldLoadProgressivelyWithIdentifier:(NSString *)identifier URL:(NSURL *)URL imageType:(NSString *)imageType originalDimensions:(CGSize)originalDimensions -{ - TIPImagePipelineTestContext *context = op.context; - context.didMakeProgressiveCheck = YES; - return context.shouldSupportProgressiveLoading; -} + NSString *tmpPipelineIdentifier = @"temp.pipeline.identifier"; + XCTestExpectation *expectation = [self expectationForNotification:TIPImagePipelineDidTearDownImagePipelineNotification object:nil handler:^BOOL(NSNotification *note) { + return [tmpPipelineIdentifier isEqualToString:note.userInfo[TIPImagePipelineImagePipelineIdentifierNotificationKey]]; + }]; -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didUpdateProgressiveImage:(id)progressiveResult progress:(float)progress -{ - TIPImagePipelineTestContext *context = op.context; - context.progressiveProgressCount++; -} + @autoreleasepool { + [[TIPImagePipelineBaseTests sharedPipeline] clearMemoryCaches]; + [[TIPImagePipelineBaseTests sharedPipeline] clearDiskCache]; + TIPImagePipeline *temporaryPipeline = [[TIPImagePipeline alloc] initWithIdentifier:tmpPipelineIdentifier]; -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFirstAnimatedImageFrame:(id)progressiveResult progress:(float)progress -{ - TIPImagePipelineTestContext *context = op.context; - context.firstAnimatedFrameProgress = progress; -} + [self runFillingTheCaches:[TIPImagePipelineBaseTests sharedPipeline] bps:1024 * kMegaBits testCacheHits:NO]; -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didUpdateProgress:(float)progress -{ - TIPImagePipelineTestContext *context = op.context; - context.normalProgressCount++; - if (context.firstProgress == 0.0f) { - context.firstProgress = progress; - } - if (!context.associatedDownloadContext) { - context.associatedDownloadContext = [op associatedDownloadContext]; - } - if (progress > context.cancelPoint) { - [op cancel]; - } - if (context.shouldCancelOnOtherContextFirstProgress && context.otherContext.firstProgress > 0.0f) { - [op cancel]; - } -} + TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFinalImage:(id)finalResult -{ - TIPImagePipelineTestContext *context = op.context; - context.finalImageContainer = finalResult.imageContainer; - context.finalSource = finalResult.imageSource; -} + XCTAssertGreaterThan([TIPImagePipelineBaseTests sharedPipeline].renderedCache.manifest.numberOfEntries, (NSUInteger)0); + dispatch_sync(globalConfig.queueForMemoryCaches, ^{ + XCTAssertGreaterThan([TIPImagePipelineBaseTests sharedPipeline].memoryCache.manifest.numberOfEntries, (NSUInteger)0); + }); + dispatch_sync(globalConfig.queueForDiskCaches, ^{ + XCTAssertGreaterThan([TIPImagePipelineBaseTests sharedPipeline].diskCache.manifest.numberOfEntries, (NSUInteger)0); + }); -- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didFailToLoadFinalImage:(NSError *)error -{ - TIPImagePipelineTestContext *context = op.context; - context.finalError = error; -} + [self runFillingTheCaches:temporaryPipeline bps:1024 * kMegaBits testCacheHits:NO]; + XCTAssertGreaterThan(temporaryPipeline.renderedCache.manifest.numberOfEntries, (NSUInteger)0); + XCTAssertEqual([TIPImagePipelineBaseTests sharedPipeline].renderedCache.manifest.numberOfEntries, (NSUInteger)0); + dispatch_sync(globalConfig.queueForMemoryCaches, ^{ + XCTAssertGreaterThan(temporaryPipeline.memoryCache.manifest.numberOfEntries, (NSUInteger)0); + XCTAssertEqual([TIPImagePipelineBaseTests sharedPipeline].memoryCache.manifest.numberOfEntries, (NSUInteger)0); + }); + dispatch_sync(globalConfig.queueForMemoryCaches, ^{ + XCTAssertGreaterThan(temporaryPipeline.diskCache.manifest.numberOfEntries, (NSUInteger)0); + XCTAssertEqual([TIPImagePipelineBaseTests sharedPipeline].diskCache.manifest.numberOfEntries, (NSUInteger)0); + }); -@end + dispatch_sync(globalConfig.queueForDiskCaches, ^{ + preDeallocDiskSize = config.internalTotalBytesForAllDiskCaches; + }); + dispatch_sync(globalConfig.queueForMemoryCaches, ^{ + preDeallocMemSize = config.internalTotalBytesForAllMemoryCaches; + }); + preDeallocRendSize = config.internalTotalBytesForAllRenderedCaches; -@implementation TIPImagePipelineTestFetchRequest + preDeallocPipelineDiskSize = (SInt64)temporaryPipeline.diskCache.totalCost; + preDeallocPipelineMemSize = (SInt64)temporaryPipeline.memoryCache.totalCost; + preDeallocPipelineRendSize = (SInt64)temporaryPipeline.renderedCache.totalCost; -- (instancetype)init -{ - self = [super init]; - if (self) { - _options = TIPImageFetchNoOptions; - _targetContentMode = UIViewContentModeCenter; - _targetDimensions = CGSizeZero; - _loadingSources = TIPImageFetchLoadingSourcesAll; + temporaryPipeline = nil; } - return self; -} -- (NSString *)cannedImageFilePath -{ - return _cannedImageFilePath ?: [TIPImagePipelineTests pathForImageOfType:self.imageType progressive:self.progressiveSource]; -} + NSLog(@"Waiting for %@", TIPImagePipelineDidTearDownImagePipelineNotification); -- (NSDictionary *)progressiveLoadingPolicies -{ - NSMutableDictionary *policies = [NSMutableDictionary dictionaryWithCapacity:2]; - if (self.jp2ProgressiveLoadingPolicy) { - policies[TIPImageTypeJPEG2000] = self.jp2ProgressiveLoadingPolicy; - } - if (self.jpegProgressiveLoadingPolicy) { - policies[TIPImageTypeJPEG] = self.jpegProgressiveLoadingPolicy; - } - return policies; -} + // Wait for the pipeline to release + [self waitForExpectationsWithTimeout:120.0 handler:^(NSError *error) { + if (error) { + NSLog(@"%@", error); + } else { + NSLog(@"Received %@", TIPImagePipelineDidTearDownImagePipelineNotification); + } + }]; + expectation = nil; -@end + __block SInt64 postDeallocDiskSize; + __block SInt64 postDeallocMemSize; + __block SInt64 postDeallocRendSize; + + const NSUInteger cacheSizeCheckMax = 30; + NSUInteger cacheSizeCheck; + for (cacheSizeCheck = 1; cacheSizeCheck <= cacheSizeCheckMax; cacheSizeCheck++) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]]; -@implementation TIPImagePipelineTestContext + dispatch_sync([TIPGlobalConfiguration sharedInstance].queueForDiskCaches, ^{ + postDeallocDiskSize = config.internalTotalBytesForAllDiskCaches; + }); + dispatch_sync([TIPGlobalConfiguration sharedInstance].queueForMemoryCaches, ^{ + postDeallocMemSize = config.internalTotalBytesForAllMemoryCaches; + }); + postDeallocRendSize = config.internalTotalBytesForAllRenderedCaches; -- (instancetype)init -{ - if (self = [super init]) { - _cancelPoint = 2.0f; + if (postDeallocDiskSize == 0 && postDeallocMemSize == 0 && postDeallocRendSize == 0) { + break; + } } - return self; -} -- (NSUInteger)expectedFrameCount -{ - if (_expectedFrameCount) { - return _expectedFrameCount; + if (cacheSizeCheck <= cacheSizeCheckMax) { + NSLog(@"Caches were relieved after %tu seconds", cacheSizeCheck); + } else { + NSLog(@"ERR: Caches were not relieved after %tu seconds", cacheSizeCheck - 1); } - return self.shouldSupportAnimatedLoading ? kFireworksFrameCount : 1; + XCTAssertEqual(postDeallocDiskSize, preDeallocDiskSize - preDeallocPipelineDiskSize); + XCTAssertEqual(postDeallocMemSize, preDeallocMemSize - preDeallocPipelineMemSize); + XCTAssertEqual(postDeallocRendSize, preDeallocRendSize - preDeallocPipelineRendSize); } @end -@implementation TIPImagePipeline (Undeprecated) - -- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context delegate:(nullable id)delegate -{ - TIPImageFetchOperation *op = [self operationWithRequest:request context:context delegate:delegate]; - [self fetchImageWithOperation:op]; - return op; -} +@implementation TIPImagePipelineTests_Three -- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context completion:(nullable TIPImagePipelineFetchCompletionBlock)completion +- (void)testFillingTheCaches { - TIPImageFetchOperation *op = [self operationWithRequest:request context:context completion:completion]; - [self fetchImageWithOperation:op]; - return op; + [self runFillingTheCaches:[TIPImagePipelineBaseTests sharedPipeline] bps:1024 * kMegaBits testCacheHits:YES]; } @end diff --git a/TwitterImagePipelineTests/TIPTestsSharedUtils.h b/TwitterImagePipelineTests/TIPTestsSharedUtils.h new file mode 100644 index 0000000..1dfb584 --- /dev/null +++ b/TwitterImagePipelineTests/TIPTestsSharedUtils.h @@ -0,0 +1,93 @@ +// +// TIPTestsSharedUtils.h +// TwitterImagePipeline +// +// Created by Nolan on 8/30/18. +// Copyright © 2018 Twitter. All rights reserved. +// + +#import +#import + +#import "TIPImageDownloader.h" + +static const uint64_t kKiloBits = 1024 * 8; +static const uint64_t kMegaBits = 1024 * kKiloBits; +static const CGSize kCarnivalImageDimensions = { (CGFloat)1880.f, (CGFloat)1253.f }; +static const CGSize kFireworksImageDimensions = { (CGFloat)480.f, (CGFloat)320.f }; +static const NSUInteger kFireworksFrameCount = 10; +static const NSTimeInterval kFireworksAnimationDurations[10] = +{ + .1f, + .15f, + .2f, + .25f, + .3f, + .35f, + .4f, + .45f, + .5f, + .55f, +}; + +@interface TIPImagePipelineTestFetchRequest : NSObject + +@property (nonatomic) NSURL *imageURL; +@property (nonatomic, copy) NSString *imageIdentifier; +@property (nonatomic, copy) TIPImageFetchHydrationBlock imageRequestHydrationBlock; +@property (nonatomic) CGSize targetDimensions; +@property (nonatomic) UIViewContentMode targetContentMode; +@property (nonatomic) NSTimeInterval timeToLive; +@property (nonatomic) TIPImageFetchOptions options; +@property (nonatomic) TIPImageFetchLoadingSources loadingSources; +@property (nonatomic) id jp2ProgressiveLoadingPolicy; +@property (nonatomic) id jpegProgressiveLoadingPolicy; + +@property (nonatomic, copy) NSString *imageType; +@property (nonatomic) BOOL progressiveSource; +@property (nonatomic, copy) NSString *cannedImageFilePath; + ++ (void)stubRequest:(TIPImagePipelineTestFetchRequest *)request + bitrate:(uint64_t)bitrate + resumable:(BOOL)resumable; + +@end + +@interface TIPImagePipelineTestContext : NSObject + +// Populated to configure behavior +@property (nonatomic) BOOL shouldCancelOnPreview; +@property (nonatomic) BOOL shouldSupportProgressiveLoading; +@property (nonatomic) BOOL shouldSupportAnimatedLoading; +@property (nonatomic) float cancelPoint; +@property (nonatomic, weak) TIPImagePipelineTestContext *otherContext; +@property (nonatomic) BOOL shouldCancelOnOtherContextFirstProgress; +@property (nonatomic) NSUInteger expectedFrameCount; + +// Populated by delegate +@property (nonatomic) BOOL didStart; +@property (nonatomic) BOOL didProvidePreviewCheck; +@property (nonatomic) BOOL didMakeProgressiveCheck; +@property (nonatomic) float firstProgress; +@property (nonatomic) float firstAnimatedFrameProgress; +@property (nonatomic) BOOL progressWasReset; +@property (nonatomic) id associatedDownloadContext; +@property (nonatomic) NSUInteger progressiveProgressCount; +@property (nonatomic) NSUInteger normalProgressCount; +@property (nonatomic) TIPImageContainer *finalImageContainer; +@property (nonatomic) NSError *finalError; +@property (nonatomic) TIPImageLoadSource finalSource; +@property (nonatomic, copy) NSArray *hitLoadSources; + +@end + +@interface TIPImagePipeline (Undeprecated) +- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context delegate:(nullable id)delegate; +- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context completion:(nullable TIPImagePipelineFetchCompletionBlock)completion; +@end + +@interface TIPImagePipelineBaseTests : XCTestCase ++ (NSString *)pathForImageOfType:(NSString *)type progressive:(BOOL)progressive; ++ (NSURL *)dummyURLWithPath:(NSString *)path; ++ (TIPImagePipeline *)sharedPipeline; +@end diff --git a/TwitterImagePipelineTests/TIPTestsSharedUtils.m b/TwitterImagePipelineTests/TIPTestsSharedUtils.m new file mode 100644 index 0000000..76f4ef8 --- /dev/null +++ b/TwitterImagePipelineTests/TIPTestsSharedUtils.m @@ -0,0 +1,283 @@ +// +// TIPTestsSharedUtils.m +// TwitterImagePipeline +// +// Created by Nolan on 8/30/18. +// Copyright © 2018 Twitter. All rights reserved. +// + +#import "TIP_Project.h" +//#import "TIPGlobalConfiguration+Project.h" +#import "TIPImageDiskCache.h" +#import "TIPImageFetchOperation+Project.h" +#import "TIPImageMemoryCache.h" +#import "TIPImagePipeline+Project.h" +#import "TIPImageRenderedCache.h" +#import "TIPTestImageFetchDownloadInternalWithStubbing.h" +#import "TIPTests.h" +#import "TIPTestsSharedUtils.h" + +@import MobileCoreServices; + +@implementation TIPImagePipelineTestFetchRequest + +- (instancetype)init +{ + self = [super init]; + if (self) { + _options = TIPImageFetchNoOptions; + _targetContentMode = UIViewContentModeCenter; + _targetDimensions = CGSizeZero; + _loadingSources = TIPImageFetchLoadingSourcesAll; + } + return self; +} + +- (NSString *)cannedImageFilePath +{ + return _cannedImageFilePath ?: [TIPImagePipelineBaseTests pathForImageOfType:self.imageType progressive:self.progressiveSource]; +} + +- (NSDictionary *)progressiveLoadingPolicies +{ + NSMutableDictionary *policies = [NSMutableDictionary dictionaryWithCapacity:2]; + if (self.jp2ProgressiveLoadingPolicy) { + policies[TIPImageTypeJPEG2000] = self.jp2ProgressiveLoadingPolicy; + } + if (self.jpegProgressiveLoadingPolicy) { + policies[TIPImageTypeJPEG] = self.jpegProgressiveLoadingPolicy; + } + return policies; +} + ++ (void)stubRequest:(TIPImagePipelineTestFetchRequest *)request + bitrate:(uint64_t)bitrate + resumable:(BOOL)resumable +{ + NSData *data = [NSData dataWithContentsOfFile:request.cannedImageFilePath options:NSDataReadingMappedIfSafe error:NULL]; + NSString *MIMEType = (NSString *)CFBridgingRelease(UTTypeIsDeclared((__bridge CFStringRef)request.imageType) ? UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)request.imageType, kUTTagClassMIMEType) : nil); + id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; + [provider addDownloadStubForRequestURL:request.imageURL + responseData:data + responseMIMEType:MIMEType + shouldSupportResuming:resumable + suggestedBitrate:bitrate]; +} + +@end + +@implementation TIPImagePipelineTestContext + +- (instancetype)init +{ + if (self = [super init]) { + _cancelPoint = 2.0f; + } + return self; +} + +- (NSUInteger)expectedFrameCount +{ + if (_expectedFrameCount) { + return _expectedFrameCount; + } + + return self.shouldSupportAnimatedLoading ? kFireworksFrameCount : 1; +} + +@end + +@implementation TIPImagePipeline (Undeprecated) + +- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context delegate:(nullable id)delegate +{ + TIPImageFetchOperation *op = [self operationWithRequest:request context:context delegate:delegate]; + [self fetchImageWithOperation:op]; + return op; +} + +- (nonnull TIPImageFetchOperation *)undeprecatedFetchImageWithRequest:(nonnull id)request context:(nullable id)context completion:(nullable TIPImagePipelineFetchCompletionBlock)completion +{ + TIPImageFetchOperation *op = [self operationWithRequest:request context:context completion:completion]; + [self fetchImageWithOperation:op]; + return op; +} + +@end + +static TIPImagePipeline *sPipeline = nil; + +@implementation TIPImagePipelineBaseTests + ++ (TIPImagePipeline *)sharedPipeline +{ + return sPipeline; +} + ++ (NSString *)pathForImageOfType:(NSString *)type progressive:(BOOL)progressive +{ + NSString *imagePath = nil; + NSBundle *thisBundle = TIPTestsResourceBundle(); + + if ([type isEqualToString:TIPImageTypeGIF]) { + imagePath = [thisBundle pathForResource:@"fireworks" ofType:@"gif"]; + } else { + NSString *extension = nil; + + if ([type isEqualToString:TIPImageTypeJPEG]) { + extension = (progressive) ? @"pjpg" : @"jpg"; + } else if ([type isEqualToString:TIPImageTypeJPEG2000]) { + extension = @"jp2"; + } else if ([type isEqualToString:TIPImageTypePNG]) { + extension = @"png"; + } + + if (extension) { + imagePath = [thisBundle pathForResource:@"carnival" ofType:extension]; + } + } + + return imagePath; +} + ++ (NSURL *)dummyURLWithPath:(NSString *)path +{ + if (!path) { + path = @""; + } + if (![path hasPrefix:@"/"]) { + path = [@"/" stringByAppendingString:path]; + } + return [NSURL URLWithString:[NSString stringWithFormat:@"http://www.dummy.com%@", path]]; +} + ++ (void)setUp +{ + TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; + TIPSetDebugSTOPOnAssertEnabled(NO); + TIPSetShouldAssertDuringPipelineRegistation(NO); + sPipeline = [[TIPImagePipeline alloc] initWithIdentifier:NSStringFromClass(self)]; + globalConfig.imageFetchDownloadProvider = [[TIPTestsImageFetchDownloadProviderOverrideClass() alloc] init]; + globalConfig.maxConcurrentImagePipelineDownloadCount = 4; + globalConfig.maxBytesForAllRenderedCaches = 12 * 1024 * 1024; + globalConfig.maxBytesForAllMemoryCaches = 36 * 1024 * 1024; + globalConfig.maxBytesForAllDiskCaches = 16 * 1024 * 1024; + globalConfig.maxRatioSizeOfCacheEntry = 0; +} + ++ (void)tearDown +{ + TIPGlobalConfiguration *globalConfig = [TIPGlobalConfiguration sharedInstance]; + TIPSetDebugSTOPOnAssertEnabled(YES); + TIPSetShouldAssertDuringPipelineRegistation(YES); + [sPipeline.renderedCache clearAllImages:NULL]; + [sPipeline.memoryCache clearAllImages:NULL]; + [sPipeline.diskCache clearAllImages:NULL]; + globalConfig.imageFetchDownloadProvider = nil; + globalConfig.maxBytesForAllRenderedCaches = -1; + globalConfig.maxBytesForAllMemoryCaches = -1; + globalConfig.maxBytesForAllDiskCaches = -1; + globalConfig.maxConcurrentImagePipelineDownloadCount = TIPMaxConcurrentImagePipelineDownloadCountDefault; + globalConfig.maxRatioSizeOfCacheEntry = -1; + + sPipeline = nil; +} + +- (void)tearDown +{ + [sPipeline.renderedCache clearAllImages:NULL]; + [sPipeline.memoryCache clearAllImages:NULL]; + [sPipeline.diskCache clearAllImages:NULL]; + + id provider = (id)[TIPGlobalConfiguration sharedInstance].imageFetchDownloadProvider; + [provider removeAllDownloadStubs]; + + // Flush ALL pipelines + __block BOOL didInspect = NO; + [[TIPGlobalConfiguration sharedInstance] inspect:^(NSDictionary *results) { + didInspect = YES; + }]; + while (!didInspect) { + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + } + + [super tearDown]; +} + +#pragma mark Delegate + +- (void)tip_imageFetchOperationDidStart:(TIPImageFetchOperation *)op +{ + TIPImagePipelineTestContext *context = op.context; + context.didStart = YES; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op willAttemptToLoadFromSource:(TIPImageLoadSource)source +{ + TIPImagePipelineTestContext *context = op.context; + NSArray *existing = context.hitLoadSources ?: @[]; + context.hitLoadSources = [existing arrayByAddingObject:@(source)]; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadPreviewImage:(id)previewResult completion:(TIPImageFetchDidLoadPreviewCallback)completion +{ + TIPImagePipelineTestContext *context = op.context; + context.didProvidePreviewCheck = YES; + + completion(context.shouldCancelOnPreview ? TIPImageFetchPreviewLoadedBehaviorStopLoading : TIPImageFetchPreviewLoadedBehaviorContinueLoading); +} + +- (BOOL)tip_imageFetchOperation:(TIPImageFetchOperation *)op shouldLoadProgressivelyWithIdentifier:(NSString *)identifier URL:(NSURL *)URL imageType:(NSString *)imageType originalDimensions:(CGSize)originalDimensions +{ + TIPImagePipelineTestContext *context = op.context; + context.didMakeProgressiveCheck = YES; + return context.shouldSupportProgressiveLoading; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didUpdateProgressiveImage:(id)progressiveResult progress:(float)progress +{ + TIPImagePipelineTestContext *context = op.context; + context.progressiveProgressCount++; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFirstAnimatedImageFrame:(id)progressiveResult progress:(float)progress +{ + TIPImagePipelineTestContext *context = op.context; + context.firstAnimatedFrameProgress = progress; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didUpdateProgress:(float)progress +{ + TIPImagePipelineTestContext *context = op.context; + context.normalProgressCount++; + if (context.firstProgress == 0.0f) { + context.firstProgress = progress; + } + if (context.firstProgress > 0.0f && progress == 0.0f) { + context.progressWasReset = YES; + } + if (!context.associatedDownloadContext) { + context.associatedDownloadContext = [op associatedDownloadContext]; + } + if (progress > context.cancelPoint) { + [op cancel]; + } + if (context.shouldCancelOnOtherContextFirstProgress && context.otherContext.firstProgress > 0.0f) { + [op cancel]; + } +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didLoadFinalImage:(id)finalResult +{ + TIPImagePipelineTestContext *context = op.context; + context.finalImageContainer = finalResult.imageContainer; + context.finalSource = finalResult.imageSource; +} + +- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op didFailToLoadFinalImage:(NSError *)error +{ + TIPImagePipelineTestContext *context = op.context; + context.finalError = error; +} + +@end diff --git a/build.sh b/build.sh index e2127e8..660b3c0 100755 --- a/build.sh +++ b/build.sh @@ -21,5 +21,4 @@ function ci_demo() { } -ci_lib "iPhone 6" && ci_demo "iPhone 6" ci_lib "iPhone 7" && ci_demo "iPhone 7"