1
1
mirror of https://github.com/kean/Nuke.git synced 2024-11-28 12:04:01 +03:00
This commit is contained in:
Alex Grebenyuk 2024-10-26 20:51:26 +00:00 committed by GitHub
commit 69951b0485
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 743 additions and 1403 deletions

View File

@ -17,7 +17,6 @@ request.processors = [.resize(width: 320)]
- ``init(url:processors:priority:options:userInfo:)``
- ``init(urlRequest:processors:priority:options:userInfo:)``
- ``init(id:data:processors:priority:options:userInfo:)``
- ``init(id:dataPublisher:processors:priority:options:userInfo:)``
- ``init(stringLiteral:)``
### Options

View File

@ -12,7 +12,6 @@
0C09B1661FE9A65700E8FE3B /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; };
0C09B1691FE9A65700E8FE3B /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; };
0C09B16F1FE9A6D800E8FE3B /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068D1BCA888800089D7F /* Helpers.swift */; };
0C0F7BF12287F6EE0034E656 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0F7BF02287F6EE0034E656 /* TaskTests.swift */; };
0C0FD5E01CA47FE1002A78FB /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D01CA47FE1002A78FB /* DataLoader.swift */; };
0C0FD5EC1CA47FE1002A78FB /* ImagePipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D31CA47FE1002A78FB /* ImagePipeline.swift */; };
0C0FD5FC1CA47FE1002A78FB /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D71CA47FE1002A78FB /* ImageCache.swift */; };
@ -20,10 +19,14 @@
0C0FD6041CA47FE1002A78FB /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D91CA47FE1002A78FB /* ImageRequest.swift */; };
0C1453A02657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; };
0C1453A12657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; };
0C16C85F2C7150C800B2A560 /* ImagePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */; };
0C16C8632C726B1B00B2A560 /* ImagePipeline+Closures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */; };
0C179C7B2283597F008AB488 /* ImageEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C179C7A2283597F008AB488 /* ImageEncoding.swift */; };
0C1B9880294E28D800C09310 /* Nuke.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C1B987F294E28D800C09310 /* Nuke.docc */; };
0C1C201D29ABBF19004B38FD /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; };
0C1C201E29ABBF19004B38FD /* Nuke.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
0C1D2D5E2C714FF900BB81B3 /* ImagePipeline+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */; };
0C1D2D5F2C71505900BB81B3 /* ImagePipelinePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */; };
0C1E620B1D6F817700AD5CF5 /* ImageRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */; };
0C1ECA421D526461009063A9 /* ImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C06871BCA888800089D7F /* ImageCacheTests.swift */; };
0C222DE3294E2DEA00012288 /* NukeUI.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C222DE2294E2DEA00012288 /* NukeUI.docc */; };
@ -108,7 +111,6 @@
0C64F73D2438371A001983C6 /* img_751.heic in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73C243836B5001983C6 /* img_751.heic */; };
0C64F73F243838BF001983C6 /* swift.png in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73E243838BF001983C6 /* swift.png */; };
0C68F609208A1F40007DC696 /* ImageDecoderRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68F608208A1F40007DC696 /* ImageDecoderRegistryTests.swift */; };
0C69FA4E1D4E222D00DA9982 /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD195291D4348AC00E011BB /* ImagePrefetcherTests.swift */; };
0C6B5BDB257010B400D763F2 /* image-p3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0C6B5BDA257010B400D763F2 /* image-p3.jpg */; };
0C6B5BE1257010D300D763F2 /* ImagePipelineFormatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6B5BE0257010D300D763F2 /* ImagePipelineFormatsTests.swift */; };
0C6CF0CD1DAF789C007B8C0E /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068E1BCA888800089D7F /* XCTestCaseExtensions.swift */; };
@ -144,7 +146,8 @@
0C8684FF20BDD578009FF7CC /* ImagePipelineProgressiveDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A8CFA20970D8D0013FD65 /* ImagePipelineProgressiveDecodingTests.swift */; };
0C86AB6A228B3B5100A81BA1 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C86AB69228B3B5100A81BA1 /* ImageTask.swift */; };
0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C880531242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift */; };
0C88C579263DAF1E0061A008 /* ImagePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */; };
0C8C614D2CCD760C00532008 /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD195291D4348AC00E011BB /* ImagePrefetcherTests.swift */; };
0C8C614E2CCD8D4500532008 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0F7BF02287F6EE0034E656 /* TaskTests.swift */; };
0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */; };
0C8D7BD51D9DBF1600D12EB7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD41D9DBF1600D12EB7 /* ViewController.swift */; };
0C8D7BD81D9DBF1600D12EB7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD61D9DBF1600D12EB7 /* Main.storyboard */; };
@ -188,14 +191,12 @@
0CA4ECCD26E68FA100BAC8E5 /* DataLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECCC26E68FA100BAC8E5 /* DataLoading.swift */; };
0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */; };
0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */; };
0CA5D954263CCEA500E08E17 /* ImagePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */; };
0CA8D8ED2958DA3700EDAA2C /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */; };
0CA8D8ED2958DA3700EDAA2C /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */; };
0CAAB0101E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; };
0CAAB0131E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; };
0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB047992856D9AC00DF9B6D /* Cache.swift */; };
0CB26802208F2565004C83F4 /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB26801208F2565004C83F4 /* DataCache.swift */; };
0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB26806208F25C2004C83F4 /* DataCacheTests.swift */; };
0CB2EFD22110F38600F7C63F /* ImagePipelineConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */; };
0CB2EFD62110F52C00F7C63F /* RateLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */; };
0CB402D525B6569700F5A241 /* TaskFetchOriginalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */; };
0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */; };
@ -231,6 +232,8 @@
0CB644C92856807F00916267 /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; };
0CB644CA2856807F00916267 /* swift.png in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73E243838BF001983C6 /* swift.png */; };
0CBA07862852DA8B00CE29F4 /* ImagePipeline+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */; };
0CC04B0A2C5698D500F1164D /* ImagePipelineActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */; };
0CC04B1C2C56AEF000F1164D /* ImagePipelineLoadDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */; };
0CC36A1925B8BC2500811018 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A1825B8BC2500811018 /* RateLimiter.swift */; };
0CC36A2525B8BC4900811018 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2425B8BC4900811018 /* Operation.swift */; };
0CC36A2C25B8BC6300811018 /* LinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2B25B8BC6300811018 /* LinkedList.swift */; };
@ -243,14 +246,11 @@
0CC6279E25C100E300466F04 /* ImageCachePerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6279D25C100E300466F04 /* ImageCachePerformanceTests.swift */; };
0CC627A525C100FA00466F04 /* ImageProcessingPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC627A425C100FA00466F04 /* ImageProcessingPerformanceTests.swift */; };
0CCBB534217D0B980026F552 /* MockProgressiveDataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCBB533217D0B980026F552 /* MockProgressiveDataLoader.swift */; };
0CD37C9A25BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */; };
0CE2D9BA2084FDDD00934B28 /* ImageDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE2D9B92084FDDD00934B28 /* ImageDecoding.swift */; };
0CE334DB2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE334DA2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift */; };
0CE3992D1D4697CE00A87D47 /* ImagePipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE3992C1D4697CE00A87D47 /* ImagePipelineTests.swift */; };
0CE5F6832156386B0046609F /* ResumableDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE5F681215638300046609F /* ResumableDataTests.swift */; };
0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */; };
0CE6202326543B6A00AAB8C3 /* TaskFetchWithPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */; };
0CE6202526543EC700AAB8C3 /* ImagePipelinePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */; };
0CE6202326543B6A00AAB8C3 /* TaskFetchWithClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */; };
0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202626546FD100AAB8C3 /* CombineExtensions.swift */; };
0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE745741D4767B900123F65 /* MockImageDecoder.swift */; };
0CF1754C22913F9800A8946E /* ImagePipeline+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */; };
@ -263,7 +263,6 @@
0CF5456B25B39A0E00B45F1E /* right-orientation.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0CF5456A25B39A0E00B45F1E /* right-orientation.jpeg */; };
0CF58FF726DAAC3800D2650D /* ImageDownsampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF58FF626DAAC3800D2650D /* ImageDownsampleTests.swift */; };
2DFD93B0233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */; };
4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -354,9 +353,11 @@
0C0FD5D81CA47FE1002A78FB /* ImageProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
0C0FD5D91CA47FE1002A78FB /* ImageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineObserver.swift; sourceTree = "<group>"; };
0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Closures.swift"; sourceTree = "<group>"; };
0C179C772282AC50008AB488 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = "<group>"; };
0C179C7A2283597F008AB488 /* ImageEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEncoding.swift; sourceTree = "<group>"; };
0C1B987F294E28D800C09310 /* Nuke.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Nuke.docc; sourceTree = "<group>"; };
0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Combine.swift"; sourceTree = "<group>"; };
0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRequestTests.swift; sourceTree = "<group>"; };
0C222DE2294E2DEA00012288 /* NukeUI.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeUI.docc; sourceTree = "<group>"; };
0C222DE4294E2E0200012288 /* NukeExtensions.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeExtensions.docc; sourceTree = "<group>"; };
@ -477,13 +478,11 @@
0CA4ECCC26E68FA100BAC8E5 /* DataLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLoading.swift; sourceTree = "<group>"; };
0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCaching.swift; sourceTree = "<group>"; };
0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCaching.swift; sourceTree = "<group>"; };
0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePublisher.swift; sourceTree = "<group>"; };
0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = "<group>"; };
0CB047992856D9AC00DF9B6D /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = "<group>"; };
0CB26801208F2565004C83F4 /* DataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = "<group>"; };
0CB26806208F25C2004C83F4 /* DataCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCacheTests.swift; sourceTree = "<group>"; };
0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineConfigurationTests.swift; sourceTree = "<group>"; };
0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiterTests.swift; sourceTree = "<group>"; };
0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalData.swift; sourceTree = "<group>"; };
0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalImage.swift; sourceTree = "<group>"; };
@ -492,6 +491,7 @@
0CB6449B28567E5400916267 /* ImageViewLoadingOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewLoadingOptionsTests.swift; sourceTree = "<group>"; };
0CB644AA28567EEA00916267 /* ImageViewExtensionsProgressiveDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewExtensionsProgressiveDecodingTests.swift; sourceTree = "<group>"; };
0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Error.swift"; sourceTree = "<group>"; };
0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineActor.swift; sourceTree = "<group>"; };
0CC36A1825B8BC2500811018 /* RateLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiter.swift; sourceTree = "<group>"; };
0CC36A2425B8BC4900811018 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
0CC36A2B25B8BC6300811018 /* LinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedList.swift; sourceTree = "<group>"; };
@ -517,8 +517,7 @@
0CE5F681215638300046609F /* ResumableDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumableDataTests.swift; sourceTree = "<group>"; };
0CE5F78720A22ABF00BC3283 /* Nuke 6 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Nuke 6 Migration Guide.md"; sourceTree = "<group>"; };
0CE5F78820A22ABF00BC3283 /* Nuke 7 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Nuke 7 Migration Guide.md"; sourceTree = "<group>"; };
0CE6202026542F7200AAB8C3 /* DataPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPublisher.swift; sourceTree = "<group>"; };
0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchWithPublisher.swift; sourceTree = "<group>"; };
0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchWithClosure.swift; sourceTree = "<group>"; };
0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelinePublisherTests.swift; sourceTree = "<group>"; };
0CE6202626546FD100AAB8C3 /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
0CE745741D4767B900123F65 /* MockImageDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockImageDecoder.swift; sourceTree = "<group>"; };
@ -528,7 +527,6 @@
0CF5456A25B39A0E00B45F1E /* right-orientation.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "right-orientation.jpeg"; sourceTree = "<group>"; };
0CF58FF626DAAC3800D2650D /* ImageDownsampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownsampleTests.swift; sourceTree = "<group>"; };
2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineProcessorTests.swift; sourceTree = "<group>"; };
4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPublisherTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -682,6 +680,7 @@
children = (
0C55FD1B28567926000FD2C9 /* ImageLoadingOptions.swift */,
0C55FD1C28567926000FD2C9 /* ImageViewExtensions.swift */,
0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */,
);
path = NukeExtensions;
sourceTree = "<group>";
@ -690,7 +689,6 @@
isa = PBXGroup;
children = (
0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */,
4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */,
0C7C06871BCA888800089D7F /* ImageCacheTests.swift */,
0C70D9772089017500A49DAC /* ImageDecoderTests.swift */,
0C68F608208A1F40007DC696 /* ImageDecoderRegistryTests.swift */,
@ -701,7 +699,6 @@
0C4AF1E91FE8551D002F86CB /* LinkedListTest.swift */,
0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */,
0C0F7BF02287F6EE0034E656 /* TaskTests.swift */,
0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */,
0C472F802654AA46007FC0F0 /* DeprecationTests.swift */,
0C91B0E82438E245007F9100 /* ImagePipelineTests */,
0C91B0EA2438E269007F9100 /* ImageProcessorsTests */,
@ -714,6 +711,8 @@
children = (
0CCBB52F217D0B6A0026F552 /* ImageViewIntegrationTests.swift */,
0C94466D1D47EC0E006DB314 /* ImageViewExtensionsTests.swift */,
0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */,
0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */,
0CB6449B28567E5400916267 /* ImageViewLoadingOptionsTests.swift */,
0CB6449928567DE000916267 /* NukeExtensionsTestsHelpers.swift */,
0CB644AA28567EEA00916267 /* ImageViewExtensionsProgressiveDecodingTests.swift */,
@ -824,7 +823,6 @@
0CC6271425BDF7A100466F04 /* ImagePipelineImageCacheTests.swift */,
0C6D0A8B20E57C810037B68F /* ImagePipelineDataCacheTests.swift */,
0C9B6E7520B9F3E2001924B8 /* ImagePipelineCoalescingTests.swift */,
0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */,
0C2A8CF620970B790013FD65 /* ImagePipelineResumableDataTests.swift */,
0C2A8CFA20970D8D0013FD65 /* ImagePipelineProgressiveDecodingTests.swift */,
2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */,
@ -833,7 +831,6 @@
0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */,
0C53C8AE263C7B1700E62D03 /* ImagePipelineDelegateTests.swift */,
0CA3BA62285C11EA0079A444 /* ImagePipelineTaskDelegateTests.swift */,
0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */,
0C5D5A9C2724773A0056B95B /* ImagePipelineAsyncAwaitTests.swift */,
0C78A2A8263F560A0051E0FF /* ImagePipelineCacheTests.swift */,
0C967EB228688B3F0050E083 /* DocumentationTests.swift */,
@ -950,7 +947,7 @@
0C2A368A26437BF100F1D000 /* TaskLoadData.swift */,
0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */,
0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */,
0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */,
0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */,
);
path = Tasks;
sourceTree = "<group>";
@ -963,6 +960,8 @@
0C53C8B0263C968200E62D03 /* ImagePipeline+Delegate.swift */,
0C78A2A6263F4E680051E0FF /* ImagePipeline+Cache.swift */,
0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */,
0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */,
0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */,
);
path = Pipeline;
sourceTree = "<group>";
@ -997,9 +996,7 @@
0CC36A3225B8BC7900811018 /* ResumableData.swift */,
0CC36A4025B8BCAC00811018 /* Log.swift */,
0C7150081FC9724C00B880AC /* Extensions.swift */,
0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */,
0CE6202026542F7200AAB8C3 /* DataPublisher.swift */,
0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */,
0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */,
0C472F822654AD69007FC0F0 /* ImageRequestKeys.swift */,
);
path = Internal;
@ -1573,6 +1570,7 @@
0C55FD1F28567926000FD2C9 /* ImageViewExtensions.swift in Sources */,
0C222DE5294E2E0300012288 /* NukeExtensions.docc in Sources */,
0C55FD1E28567926000FD2C9 /* ImageLoadingOptions.swift in Sources */,
0C1D2D5E2C714FF900BB81B3 /* ImagePipeline+Combine.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1580,6 +1578,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0C16C85F2C7150C800B2A560 /* ImagePublisherTests.swift in Sources */,
0CB6449828567DCA00916267 /* CombineExtensions.swift in Sources */,
0CB6449728567DCA00916267 /* NukeExtensions.swift in Sources */,
0C55FD2728567C12000FD2C9 /* ImageViewExtensionsTests.swift in Sources */,
@ -1598,6 +1597,7 @@
0CB6448C28567DC300916267 /* MockDataCache.swift in Sources */,
0CB6449628567DCA00916267 /* XCTestCaseExtensions.swift in Sources */,
0CB6448F28567DC300916267 /* MockImageEncoder.swift in Sources */,
0C1D2D5F2C71505900BB81B3 /* ImagePipelinePublisherTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1616,10 +1616,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */,
0CD37C9A25BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift in Sources */,
0CC04B1C2C56AEF000F1164D /* ImagePipelineLoadDataTests.swift in Sources */,
0C75279F1D473AEF00EC6222 /* MockImageProcessor.swift in Sources */,
0C69FA4E1D4E222D00DA9982 /* ImagePrefetcherTests.swift in Sources */,
0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */,
0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */,
0C91B0F02438E352007F9100 /* RoundedCornersTests.swift in Sources */,
@ -1628,8 +1626,6 @@
0CAAB0101E45D6DA00924450 /* NukeExtensions.swift in Sources */,
0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */,
0C70D9782089017500A49DAC /* ImageDecoderTests.swift in Sources */,
0C88C579263DAF1E0061A008 /* ImagePublisherTests.swift in Sources */,
0CB2EFD22110F38600F7C63F /* ImagePipelineConfigurationTests.swift in Sources */,
0C7082612640521900C62638 /* MockImageEncoder.swift in Sources */,
0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */,
0C6B5BE1257010D300D763F2 /* ImagePipelineFormatsTests.swift in Sources */,
@ -1639,6 +1635,7 @@
0C75279E1D473AEF00EC6222 /* MockImageCache.swift in Sources */,
0CCBB534217D0B980026F552 /* MockProgressiveDataLoader.swift in Sources */,
0C7CE28F24393ACC0018C8C3 /* CoreImageFilterTests.swift in Sources */,
0C8C614D2CCD760C00532008 /* ImagePrefetcherTests.swift in Sources */,
0C53C8AF263C7B1700E62D03 /* ImagePipelineDelegateTests.swift in Sources */,
0C1453A02657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */,
0C1E620B1D6F817700AD5CF5 /* ImageRequestTests.swift in Sources */,
@ -1655,6 +1652,7 @@
0C91B0F42438E38B007F9100 /* CompositionTests.swift in Sources */,
0C91B0F62438E3CB007F9100 /* GaussianBlurTests.swift in Sources */,
0C6D0A8820E574400037B68F /* MockDataCache.swift in Sources */,
0C8C614E2CCD8D4500532008 /* TaskTests.swift in Sources */,
0C472F812654AA46007FC0F0 /* DeprecationTests.swift in Sources */,
0C9B6E7620B9F3E2001924B8 /* ImagePipelineCoalescingTests.swift in Sources */,
0C91B0F22438E374007F9100 /* AnonymousTests.swift in Sources */,
@ -1666,9 +1664,7 @@
0CA3BA63285C11EA0079A444 /* ImagePipelineTaskDelegateTests.swift in Sources */,
0C6D0A8C20E57C810037B68F /* ImagePipelineDataCacheTests.swift in Sources */,
0C68F609208A1F40007DC696 /* ImageDecoderRegistryTests.swift in Sources */,
0CE6202526543EC700AAB8C3 /* ImagePipelinePublisherTests.swift in Sources */,
0C91B0EE2438E307007F9100 /* CircleTests.swift in Sources */,
0C0F7BF12287F6EE0034E656 /* TaskTests.swift in Sources */,
0CC6271525BDF7A100466F04 /* ImagePipelineImageCacheTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1717,7 +1713,6 @@
0CC36A2C25B8BC6300811018 /* LinkedList.swift in Sources */,
0C179C7B2283597F008AB488 /* ImageEncoding.swift in Sources */,
0CB4030125B6639200F5A241 /* TaskLoadImage.swift in Sources */,
0CA5D954263CCEA500E08E17 /* ImagePublisher.swift in Sources */,
0CA4ECCD26E68FA100BAC8E5 /* DataLoading.swift in Sources */,
0CA4ECAF26E683FD00BAC8E5 /* ImageEncoders+Default.swift in Sources */,
0CC36A2525B8BC4900811018 /* Operation.swift in Sources */,
@ -1725,6 +1720,7 @@
0C78A2A7263F4E680051E0FF /* ImagePipeline+Cache.swift in Sources */,
0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */,
0CA4ECCA26E6868300BAC8E5 /* ImageProcessingOptions.swift in Sources */,
0CC04B0A2C5698D500F1164D /* ImagePipelineActor.swift in Sources */,
0C53C8B1263C968200E62D03 /* ImagePipeline+Delegate.swift in Sources */,
0CA4ECBC26E6856300BAC8E5 /* ImageDecompression.swift in Sources */,
0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */,
@ -1732,20 +1728,20 @@
0CA4ECC826E6864D00BAC8E5 /* ImageProcessors+RoundedCorners.swift in Sources */,
0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */,
0C472F842654AD88007FC0F0 /* ImageRequestKeys.swift in Sources */,
0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */,
0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */,
0CA4ECB626E6846800BAC8E5 /* ImageProcessors+Resize.swift in Sources */,
0C1B9880294E28D800C09310 /* Nuke.docc in Sources */,
0CC36A3325B8BC7900811018 /* ResumableData.swift in Sources */,
0CE6202326543B6A00AAB8C3 /* TaskFetchWithPublisher.swift in Sources */,
0CE6202326543B6A00AAB8C3 /* TaskFetchWithClosure.swift in Sources */,
0CA4ECC426E685F500BAC8E5 /* ImageProcessors+GaussianBlur.swift in Sources */,
0CA4EC9B26E67D3000BAC8E5 /* ImageDecoders+Empty.swift in Sources */,
0CB26802208F2565004C83F4 /* DataCache.swift in Sources */,
0C16C8632C726B1B00B2A560 /* ImagePipeline+Closures.swift in Sources */,
0CA4EC9F26E67D6200BAC8E5 /* ImageDecoderRegistry.swift in Sources */,
0CA4ECBA26E6850B00BAC8E5 /* Graphics.swift in Sources */,
0CA4ECB426E6844B00BAC8E5 /* ImageProcessors.swift in Sources */,
0C2A368B26437BF100F1D000 /* TaskLoadData.swift in Sources */,
0CA8D8ED2958DA3700EDAA2C /* Atomic.swift in Sources */,
0CA8D8ED2958DA3700EDAA2C /* Mutex.swift in Sources */,
0C0FD6041CA47FE1002A78FB /* ImageRequest.swift in Sources */,
0CA4EC9926E67CEC00BAC8E5 /* ImageDecoders+Default.swift in Sources */,
0CA4ECC226E685E100BAC8E5 /* ImageProcessors+Composition.swift in Sources */,

View File

@ -48,14 +48,6 @@ public final class DataCache: DataCaching, @unchecked Sendable {
/// The time interval between cache sweeps. The default value is 1 hour.
public var sweepInterval: TimeInterval = 3600
// Deprecated in Nuke 12.2
@available(*, deprecated, message: "It's not recommended to use compression with the popular image formats that already compress the data")
public var isCompressionEnabled: Bool {
get { _isCompressionEnabled }
set { _isCompressionEnabled = newValue }
}
var _isCompressionEnabled = false
// Staging
private let lock = NSLock()
@ -143,7 +135,7 @@ public final class DataCache: DataCaching, @unchecked Sendable {
guard let url = url(for: key) else {
return nil
}
return try? decompressed(Data(contentsOf: url))
return try? Data(contentsOf: url)
}
/// Returns `true` if the cache contains the data for the given key.
@ -322,33 +314,17 @@ public final class DataCache: DataCaching, @unchecked Sendable {
switch change.type {
case let .add(data):
do {
try compressed(data).write(to: url)
try data.write(to: url)
} catch let error as NSError {
guard error.code == CocoaError.fileNoSuchFile.rawValue && error.domain == CocoaError.errorDomain else { return }
try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil)
try? compressed(data).write(to: url) // re-create a directory and try again
try? data.write(to: url) // re-create a directory and try again
}
case .remove:
try? FileManager.default.removeItem(at: url)
}
}
// MARK: Compression
private func compressed(_ data: Data) throws -> Data {
guard _isCompressionEnabled else {
return data
}
return try (data as NSData).compressed(using: .lzfse) as Data
}
private func decompressed(_ data: Data) throws -> Data {
guard _isCompressionEnabled else {
return data
}
return try (data as NSData).decompressed(using: .lzfse) as Data
}
// MARK: Sweep
/// Synchronously performs a cache sweep and removes the least recently items

View File

@ -54,7 +54,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable {
}
/// Image decoding context used when selecting which decoder to use.
public struct ImageDecodingContext: @unchecked Sendable {
public struct ImageDecodingContext: Sendable {
public var request: ImageRequest
public var data: Data
/// Returns `true` if the download was completed.

View File

@ -30,7 +30,7 @@ extension ImageEncoders {
self.compressionRatio = compressionRatio
}
private static let availability = Atomic<[AssetType: Bool]>(value: [:])
private static let availability = Mutex<[AssetType: Bool]>([:])
/// Returns `true` if the encoding is available for the given format on
/// the current hardware. Some of the most recent formats might not be

View File

@ -33,7 +33,7 @@ extension ImageEncoding {
}
/// Image encoding context used when selecting which encoder to use.
public struct ImageEncodingContext: @unchecked Sendable {
public struct ImageEncodingContext: Sendable {
public let request: ImageRequest
public let image: PlatformImage
public let urlResponse: URLResponse?

View File

@ -19,7 +19,7 @@ public typealias PlatformImage = NSImage
#endif
/// An image container with an image and associated metadata.
public struct ImageContainer: @unchecked Sendable {
public struct ImageContainer: Sendable {
#if os(macOS)
/// A fetched image.
public var image: NSImage {

View File

@ -69,7 +69,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri
switch ref.resource {
case .url(let url): return url.map { URLRequest(url: $0) } // create lazily
case .urlRequest(let urlRequest): return urlRequest
case .publisher: return nil
case .closure: return nil
}
}
@ -80,7 +80,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri
switch ref.resource {
case .url(let url): return url
case .urlRequest(let request): return request.url
case .publisher: return nil
case .closure: return nil
}
}
@ -202,51 +202,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri
// pipeline by using a custom DataLoader and passing an async function in
// the request userInfo. g
self.ref = Container(
resource: .publisher(DataPublisher(id: id, data)),
processors: processors,
priority: priority,
options: options,
userInfo: userInfo
)
}
/// Initializes a request with the given data publisher.
///
/// For example, here is how you can use it with the Photos framework (the
/// `imageDataPublisher` API is a custom convenience extension not included
/// in the framework).
///
/// ```swift
/// let request = ImageRequest(
/// id: asset.localIdentifier,
/// dataPublisher: PHAssetManager.imageDataPublisher(for: asset)
/// )
/// ```
///
/// - important: If you are using a pipeline with a custom configuration that
/// enables aggressive disk cache, fetched data will be stored in this cache.
/// You can use ``Options-swift.struct/disableDiskCache`` to disable it.
///
/// - parameters:
/// - id: Uniquely identifies the fetched image.
/// - data: A data publisher to be used for fetching image data.
/// - processors: Processors to be apply to the image. See <doc:image-processing> to learn more.
/// - priority: The priority of the request, ``Priority-swift.enum/normal`` by default.
/// - options: Image loading options.
/// - userInfo: Custom info passed alongside the request.
public init<P>(
id: String,
dataPublisher: P,
processors: [any ImageProcessing] = [],
priority: Priority = .normal,
options: Options = [],
userInfo: [UserInfoKey: Any]? = nil
) where P: Publisher, P.Output == Data {
// It could technically be implemented without any special change to the
// pipeline by using a custom DataLoader and passing a publisher in the
// request userInfo.
self.ref = Container(
resource: .publisher(DataPublisher(id: id, dataPublisher)),
resource: .closure(data, id: id),
processors: processors,
priority: priority,
options: options,
@ -470,8 +426,8 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri
(ref.userInfo?[.scaleKey] as? NSNumber)?.floatValue
}
var publisher: DataPublisher? {
if case .publisher(let publisher) = ref.resource { return publisher }
var closure: (@Sendable () async throws -> Data)? {
if case .closure(let closure, _) = ref.resource { return closure }
return nil
}
}
@ -519,13 +475,13 @@ extension ImageRequest {
enum Resource: CustomStringConvertible {
case url(URL?)
case urlRequest(URLRequest)
case publisher(DataPublisher)
case closure(@Sendable () async throws -> Data, id: String)
var description: String {
switch self {
case .url(let url): return "\(url?.absoluteString ?? "nil")"
case .urlRequest(let urlRequest): return "\(urlRequest)"
case .publisher(let data): return "\(data)"
case .closure(_, let id): return id
}
}
@ -533,7 +489,7 @@ extension ImageRequest {
switch self {
case .url(let url): return url?.absoluteString
case .urlRequest(let urlRequest): return urlRequest.url?.absoluteString
case .publisher(let publisher): return publisher.id
case .closure(_, let id): return id
}
}
}

View File

@ -13,7 +13,7 @@ import AppKit
#endif
/// An image response that contains a fetched image and some metadata.
public struct ImageResponse: @unchecked Sendable {
public struct ImageResponse: Sendable {
/// An image container with an image and associated metadata.
public var container: ImageContainer

View File

@ -18,7 +18,7 @@ import AppKit
/// The pipeline maintains a strong reference to the task until the request
/// finishes or fails; you do not need to maintain a reference to the task unless
/// it is useful for your app.
public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Sendable {
public final class ImageTask: Hashable, @unchecked Sendable {
/// An identifier that uniquely identifies the task within a given pipeline.
/// Unique only within that pipeline.
public let taskId: Int64
@ -29,14 +29,14 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
/// The priority of the task. The priority can be updated dynamically even
/// for a task that is already running.
public var priority: ImageRequest.Priority {
get { withLock { $0.priority } }
get { nonisolatedState.withLock { $0.priority } }
set { setPriority(newValue) }
}
/// Returns the current download progress. Returns zeros before the download
/// is started and the expected size of the resource is known.
public var currentProgress: Progress {
withLock { $0.progress }
nonisolatedState.withLock { $0.progress }
}
/// The download progress.
@ -59,12 +59,11 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
}
/// The current state of the task.
public var state: State {
withLock { $0.state }
}
@ImagePipelineActor
public var state: State = .running
/// The state of the image task.
public enum State {
public enum State: Sendable {
/// The task is currently running.
case running
/// The task has received a cancel message.
@ -73,6 +72,11 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
case completed
}
/// Returns `true` if the task cancellation is initiated.
public var isCancelling: Bool {
nonisolatedState.withLock { $0.isCancelling }
}
// MARK: - Async/Await
/// Returns the response image.
@ -86,7 +90,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
public var response: ImageResponse {
get async throws {
try await withTaskCancellationHandler {
try await _task.value
try await task.value
} onCancel: {
cancel()
}
@ -132,35 +136,42 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
case finished(Result<ImageResponse, ImagePipeline.Error>)
}
private var publicState: PublicState
private let nonisolatedState: Mutex<ImageTaskState>
private let isDataTask: Bool
private let onEvent: ((Event, ImageTask) -> Void)?
private let lock: os_unfair_lock_t
private let queue: DispatchQueue
private var task: Task<ImageResponse, Error>!
private weak var pipeline: ImagePipeline?
// State synchronized on `pipeline.queue`.
var _task: Task<ImageResponse, Error>!
var _continuation: UnsafeContinuation<ImageResponse, Error>?
var _state: State = .running
private var _events: PassthroughSubject<Event, Never>?
@ImagePipelineActor
var continuation: UnsafeContinuation<ImageResponse, Error>?
deinit {
lock.deinitialize(count: 1)
lock.deallocate()
}
@ImagePipelineActor
var _events: PassthroughSubject<ImageTask.Event, Never>?
init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) {
self.taskId = taskId
self.request = request
self.publicState = PublicState(priority: request.priority)
self.nonisolatedState = Mutex(ImageTaskState(priority: request.priority))
self.isDataTask = isDataTask
self.pipeline = pipeline
self.queue = pipeline.queue
self.onEvent = onEvent
self.task = Task {
try await perform()
}
}
lock = .allocate(capacity: 1)
lock.initialize(to: os_unfair_lock())
@ImagePipelineActor
private func perform() async throws -> ImageResponse {
try await withUnsafeThrowingContinuation {
continuation = $0
// The task gets started asynchronously in a `Task` and cancellation
// can happen before the pipeline reaches `startImageTask`. In that
// case, the `cancel` method do no send the task event.
guard state != .cancelled else {
return dispatch(.cancelled) // Important to set after continuation
}
pipeline?.startImageTask(self, isDataTask: isDataTask)
}
}
/// Marks task as being cancelled.
@ -168,90 +179,77 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
/// The pipeline will immediately cancel any work associated with a task
/// unless there is an equivalent outstanding task running.
public func cancel() {
let didChange: Bool = withLock {
guard $0.state == .running else { return false }
$0.state = .cancelled
guard nonisolatedState.withLock({
guard !$0.isCancelling else { return false }
$0.isCancelling = true
return true
}) else { return }
Task { @ImagePipelineActor in
pipeline?.cancelImageTask(self)
}
guard didChange else { return } // Make sure it gets called once (expensive)
pipeline?.imageTaskCancelCalled(self)
}
private func setPriority(_ newValue: ImageRequest.Priority) {
let didChange: Bool = withLock {
guard nonisolatedState.withLock({
guard $0.priority != newValue else { return false }
$0.priority = newValue
return $0.state == .running
return !$0.isCancelling
}) else { return }
Task { @ImagePipelineActor in
pipeline?.imageTask(self, didChangePriority: newValue)
}
guard didChange else { return }
pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue)
}
// MARK: Internals
/// Gets called when the task is cancelled either by the user or by an
/// external event such as session invalidation.
///
/// synchronized on `pipeline.queue`.
@ImagePipelineActor
func _cancel() {
guard _setState(.cancelled) else { return }
_dispatch(.cancelled)
guard state == .running else { return }
state = .cancelled
dispatch(.cancelled)
}
/// Gets called when the associated task sends a new event.
///
/// synchronized on `pipeline.queue`.
func _process(_ event: AsyncTask<ImageResponse, ImagePipeline.Error>.Event) {
@ImagePipelineActor
func process(_ event: AsyncTask<ImageResponse, ImagePipeline.Error>.Event) {
guard state == .running else { return }
switch event {
case let .value(response, isCompleted):
if isCompleted {
_finish(.success(response))
state = .completed
dispatch(.finished(.success(response)))
} else {
_dispatch(.preview(response))
dispatch(.preview(response))
}
case let .progress(value):
withLock { $0.progress = value }
_dispatch(.progress(value))
nonisolatedState.withLock { $0.progress = value }
dispatch(.progress(value))
case let .error(error):
_finish(.failure(error))
state = .completed
dispatch(.finished(.failure(error)))
}
}
/// Synchronized on `pipeline.queue`.
private func _finish(_ result: Result<ImageResponse, ImagePipeline.Error>) {
guard _setState(.completed) else { return }
_dispatch(.finished(result))
}
/// Synchronized on `pipeline.queue`.
func _setState(_ state: State) -> Bool {
guard _state == .running else { return false }
_state = state
if onEvent == nil {
withLock { $0.state = state }
}
return true
}
/// Dispatches the given event to the observers.
///
/// - warning: The task needs to be fully wired (`_continuation` present)
/// before it can start sending the events.
///
/// synchronized on `pipeline.queue`.
func _dispatch(_ event: Event) {
guard _continuation != nil else {
@ImagePipelineActor
private func dispatch(_ event: Event) {
guard continuation != nil else {
return // Task isn't fully wired yet
}
_events?.send(event)
switch event {
case .cancelled:
_events?.send(completion: .finished)
_continuation?.resume(throwing: CancellationError())
continuation?.resume(throwing: CancellationError())
case .finished(let result):
let result = result.mapError { $0 as Error }
_events?.send(completion: .finished)
_continuation?.resume(with: result)
continuation?.resume(with: result)
default:
break
}
@ -269,27 +267,18 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send
public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool {
ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
// MARK: CustomStringConvertible
public var description: String {
"ImageTask(id: \(taskId), priority: \(priority), progress: \(currentProgress.completed) / \(currentProgress.total), state: \(state))"
}
}
@available(*, deprecated, renamed: "ImageTask", message: "Async/Await support was added directly to the existing `ImageTask` type")
public typealias AsyncImageTask = ImageTask
// MARK: - ImageTask (Private)
extension ImageTask {
private func makeStream<T>(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream<T> {
AsyncStream { continuation in
self.queue.async {
guard let events = self._makeEventsSubject() else {
Task { @ImagePipelineActor in
guard state == .running else {
return continuation.finish()
}
let cancellable = events.sink { _ in
let cancellable = makeEvents().sink { _ in
continuation.finish()
} receiveValue: { event in
if let value = closure(event) {
@ -309,29 +298,17 @@ extension ImageTask {
}
}
// Synchronized on `pipeline.queue`
private func _makeEventsSubject() -> PassthroughSubject<Event, Never>? {
guard _state == .running else {
return nil
}
@ImagePipelineActor
private func makeEvents() -> PassthroughSubject<ImageTask.Event, Never> {
if _events == nil {
_events = PassthroughSubject()
}
return _events!
}
private func withLock<T>(_ closure: (inout PublicState) -> T) -> T {
os_unfair_lock_lock(lock)
defer { os_unfair_lock_unlock(lock) }
return closure(&publicState)
}
/// Contains the state synchronized using the internal lock.
///
/// - warning: Must be accessed using `withLock`.
private struct PublicState {
var state: ImageTask.State = .running
var priority: ImageRequest.Priority
var progress = Progress(completed: 0, total: 0)
}
}
private struct ImageTaskState {
var isCancelling = false
var priority: ImageRequest.Priority
var progress = ImageTask.Progress(completed: 0, total: 0)
}

View File

@ -1,60 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
@preconcurrency import Combine
final class DataPublisher {
let id: String
private let _sink: (@escaping ((PublisherCompletion) -> Void), @escaping ((Data) -> Void)) -> any Cancellable
init<P: Publisher>(id: String, _ publisher: P) where P.Output == Data {
self.id = id
self._sink = { onCompletion, onValue in
let cancellable = publisher.sink(receiveCompletion: {
switch $0 {
case .finished: onCompletion(.finished)
case .failure(let error): onCompletion(.failure(error))
}
}, receiveValue: {
onValue($0)
})
return AnonymousCancellable { cancellable.cancel() }
}
}
convenience init(id: String, _ data: @Sendable @escaping () async throws -> Data) {
self.init(id: id, publisher(from: data))
}
func sink(receiveCompletion: @escaping ((PublisherCompletion) -> Void), receiveValue: @escaping ((Data) -> Void)) -> any Cancellable {
_sink(receiveCompletion, receiveValue)
}
}
private func publisher(from closure: @Sendable @escaping () async throws -> Data) -> AnyPublisher<Data, Error> {
Deferred {
Future { promise in
let promise = UncheckedSendableBox(value: promise)
Task {
do {
let data = try await closure()
promise.value(.success(data))
} catch {
promise.value(.failure(error))
}
}
}
}.eraseToAnyPublisher()
}
enum PublisherCompletion {
case finished
case failure(Error)
}
/// - warning: Avoid using it!
struct UncheckedSendableBox<Value>: @unchecked Sendable {
let value: Value
}

View File

@ -49,15 +49,3 @@ extension ImageRequest.Priority {
}
}
}
final class AnonymousCancellable: Cancellable {
let onCancel: @Sendable () -> Void
init(_ onCancel: @Sendable @escaping () -> Void) {
self.onCancel = onCancel
}
func cancel() {
onCancel()
}
}

View File

@ -78,7 +78,7 @@ struct TaskFetchOriginalDataKey: Hashable {
init(_ request: ImageRequest) {
self.imageId = request.imageId
switch request.resource {
case .url, .publisher:
case .url, .closure:
self.cachePolicy = .useProtocolCachePolicy
self.allowsCellularAccess = true
case let .urlRequest(urlRequest):

View File

@ -24,7 +24,7 @@ func signpost<T>(_ name: StaticString, _ work: () throws -> T) rethrows -> T {
return result
}
private let log = Atomic(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading"))
private let log = Mutex(OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading"))
enum Formatter {
static func bytes(_ count: Int) -> String {

View File

@ -4,11 +4,11 @@
import Foundation
final class Atomic<T>: @unchecked Sendable {
final class Mutex<T>: @unchecked Sendable {
private var _value: T
private let lock: os_unfair_lock_t
init(value: T) {
init(_ value: T) {
self._value = value
self.lock = .allocate(capacity: 1)
self.lock.initialize(to: os_unfair_lock())
@ -38,3 +38,13 @@ final class Atomic<T>: @unchecked Sendable {
return closure(&_value)
}
}
extension Mutex where T: BinaryInteger {
func incremented() -> T {
withLock {
let value = $0
$0 += 1
return value
}
}
}

View File

@ -13,12 +13,12 @@ import Foundation
/// The implementation supports quick bursts of requests which can be executed
/// without any delays when "the bucket is full". This is important to prevent
/// rate limiter from affecting "normal" requests flow.
final class RateLimiter: @unchecked Sendable {
@ImagePipelineActor
final class RateLimiter {
// This type isn't really Sendable and requires the caller to use the same
// queue as it does for synchronization.
private let bucket: TokenBucket
private let queue: DispatchQueue
private var bucket: TokenBucket
private var pending = LinkedList<Work>() // fast append, fast remove first
private var isExecutingPendingTasks = false
@ -30,8 +30,7 @@ final class RateLimiter: @unchecked Sendable {
/// - rate: Maximum number of requests per second. 80 by default.
/// - burst: Maximum number of requests which can be executed without any
/// delays when "bucket is full". 25 by default.
init(queue: DispatchQueue, rate: Int = 80, burst: Int = 25) {
self.queue = queue
nonisolated init(rate: Int = 80, burst: Int = 25) {
self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst))
}
@ -56,7 +55,10 @@ final class RateLimiter: @unchecked Sendable {
let bucketRate = 1000.0 / bucket.rate
let delay = Int(2.1 * bucketRate) // 14 ms for rate 80 (default)
let bounds = min(100, max(15, delay))
queue.asyncAfter(deadline: .now() + .milliseconds(bounds)) { self.executePendingTasks() }
Task {
try? await Task.sleep(nanoseconds: UInt64(bounds) * 1_000_000)
self.executePendingTasks()
}
}
private func executePendingTasks() {
@ -70,7 +72,7 @@ final class RateLimiter: @unchecked Sendable {
}
}
private final class TokenBucket {
private struct TokenBucket {
let rate: Double
private let burst: Double // maximum bucket size
private var bucket: Double
@ -86,7 +88,7 @@ private final class TokenBucket {
}
/// Returns `true` if the closure was executed, `false` if dropped.
func execute(_ work: () -> Bool) -> Bool {
mutating func execute(_ work: () -> Bool) -> Bool {
refill()
guard bucket >= 1.0 else {
return false // bucket is empty
@ -97,7 +99,7 @@ private final class TokenBucket {
return true
}
private func refill() {
private mutating func refill() {
let now = CFAbsoluteTimeGetCurrent()
bucket += rate * max(0, now - timestamp) // rate * (time delta)
timestamp = now

View File

@ -6,7 +6,7 @@ import Foundation
/// Resumable data support. For more info see:
/// - https://developer.apple.com/library/content/qa/qa1761/_index.html
struct ResumableData: @unchecked Sendable {
struct ResumableData: Sendable {
let data: Data
let validator: String // Either Last-Modified or ETag
@ -67,29 +67,29 @@ final class ResumableDataStorage: @unchecked Sendable {
static let shared = ResumableDataStorage()
private let lock = NSLock()
private var registeredPipelines = Set<UUID>()
private var namespaces = Set<UUID>()
private var cache: Cache<Key, ResumableData>?
// MARK: Registration
func register(_ pipeline: ImagePipeline) {
func register(_ namespace: UUID) {
lock.lock()
defer { lock.unlock() }
if registeredPipelines.isEmpty {
if namespaces.isEmpty {
// 32 MB
cache = Cache(costLimit: 32000000, countLimit: 100)
}
registeredPipelines.insert(pipeline.id)
namespaces.insert(namespace)
}
func unregister(_ pipeline: ImagePipeline) {
func unregister(_ namespace: UUID) {
lock.lock()
defer { lock.unlock() }
registeredPipelines.remove(pipeline.id)
if registeredPipelines.isEmpty {
namespaces.remove(namespace)
if namespaces.isEmpty {
cache = nil // Deallocate storage
}
}
@ -103,31 +103,31 @@ final class ResumableDataStorage: @unchecked Sendable {
// MARK: Storage
func removeResumableData(for request: ImageRequest, pipeline: ImagePipeline) -> ResumableData? {
func removeResumableData(for request: ImageRequest, namespace: UUID) -> ResumableData? {
lock.lock()
defer { lock.unlock() }
guard let key = Key(request: request, pipeline: pipeline) else { return nil }
guard let key = Key(request: request, namespace: namespace) else { return nil }
return cache?.removeValue(forKey: key)
}
func storeResumableData(_ data: ResumableData, for request: ImageRequest, pipeline: ImagePipeline) {
func storeResumableData(_ data: ResumableData, for request: ImageRequest, namespace: UUID) {
lock.lock()
defer { lock.unlock() }
guard let key = Key(request: request, pipeline: pipeline) else { return }
guard let key = Key(request: request, namespace: namespace) else { return }
cache?.set(data, forKey: key, cost: data.data.count)
}
private struct Key: Hashable {
let pipelineId: UUID
let namespace: UUID
let imageId: String
init?(request: ImageRequest, pipeline: ImagePipeline) {
init?(request: ImageRequest, namespace: UUID) {
guard let imageId = request.imageId else {
return nil
}
self.pipelineId = pipeline.id
self.namespace = namespace
self.imageId = imageId
}
}

View File

@ -90,9 +90,23 @@ public final class DataLoader: DataLoading, @unchecked Sendable {
#endif
}()
public func loadData(with request: URLRequest,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Swift.Error?) -> Void) -> any Cancellable {
public func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> {
AsyncThrowingStream { continuation in
let task = loadData(with: request) { data, response in
continuation.yield((data, response))
} completion: { error in
continuation.finish(throwing: error)
}
continuation.onTermination = { reason in
switch reason {
case .cancelled: task.cancel()
default: break
}
}
}
}
private func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Swift.Error?) -> Void) -> URLSessionTask {
let task = session.dataTask(with: request)
if #available(iOS 14.5, tvOS 14.5, watchOS 7.4, macOS 11.3, *) {
task.prefersIncrementalDelivery = prefersIncrementalDelivery
@ -130,13 +144,13 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Se
func loadData(with task: URLSessionDataTask,
session: URLSession,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Error?) -> Void) -> any Cancellable {
completion: @escaping (Error?) -> Void) -> URLSessionTask {
let handler = _Handler(didReceiveData: didReceiveData, completion: completion)
session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue
self.handlers[task] = handler
}
task.resume()
return AnonymousCancellable { task.cancel() }
return task
}
// MARK: URLSessionDelegate
@ -223,6 +237,7 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Se
private final class _Handler: @unchecked Sendable {
let didReceiveData: (Data, URLResponse) -> Void
let completion: (Error?) -> Void
var resumableData: Data?
init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) {
self.didReceiveData = didReceiveData

View File

@ -6,16 +6,9 @@ import Foundation
/// Fetches original image data.
public protocol DataLoading: Sendable {
/// - parameter didReceiveData: Can be called multiple times if streaming
/// Returns data for the given request.
///
/// - returns: Sequence that can be called more than once if streaming
/// is supported.
/// - parameter completion: Must be called once after all (or none in case
/// of an error) `didReceiveData` closures have been called.
func loadData(with request: URLRequest,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Error?) -> Void) -> any Cancellable
}
/// A unit of work that can be cancelled.
public protocol Cancellable: AnyObject, Sendable {
func cancel()
func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error>
}

View File

@ -0,0 +1,107 @@
// The MIT License (MIT)
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
extension ImagePipeline {
/// Loads an image for the given request.
///
/// - warning: Soft-deprecated in Nuke 13.0.
///
/// - parameters:
/// - request: An image request.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public nonisolated func loadImage(
with url: URL,
completion: @MainActor @Sendable @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: ImageRequest(url: url), progress: nil, completion: completion)
}
/// Loads an image for the given request.
///
/// - warning: Soft-deprecated in Nuke 13.0.
///
/// - parameters:
/// - request: An image request.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public nonisolated func loadImage(
with request: ImageRequest,
completion: @MainActor @Sendable @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: request, progress: nil, completion: completion)
}
/// Loads an image for the given request.
///
/// - warning: Soft-deprecated in Nuke 13.0.
///
/// - parameters:
/// - request: An image request.
/// - progress: A closure to be called periodically on the main thread when
/// the progress is updated.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public nonisolated func loadImage(
with request: ImageRequest,
progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?,
completion: @MainActor @Sendable @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: request, progress: {
progress?($0, $1.completed, $1.total)
}, completion: completion)
}
/// Loads the image data for the given request. The data doesn't get decoded
/// or processed in any other way.
///
/// You can call ``loadImage(with:completion:)-43osv`` for the request at any point after calling
/// ``loadData(with:completion:)-6cwk3``, the pipeline will use the same operation to load the data,
/// no duplicated work will be performed.
///
/// - warning: Soft-deprecated in Nuke 13.0.
///
/// - parameters:
/// - request: An image request.
/// - progress: A closure to be called periodically on the main thread when the progress is updated.
/// - completion: A closure to be called on the main thread when the request is finished.
@discardableResult public nonisolated func loadData(
with request: ImageRequest,
progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)? = nil,
completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void
) -> ImageTask {
_loadImage(with: request, isDataTask: true) { _, progress in
progressHandler?(progress.completed, progress.total)
} completion: { result in
let result = result.map { response in
// Data should never be empty
(data: response.container.data ?? Data(), response: response.urlResponse)
}
completion(result)
}
}
private nonisolated func _loadImage(
with request: ImageRequest,
isDataTask: Bool = false,
progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?,
completion: @MainActor @Sendable @escaping (Result<ImageResponse, Error>) -> Void
) -> ImageTask {
makeImageTask(with: request, isDataTask: isDataTask) { event, task in
DispatchQueue.main.async {
// The callback-based API guarantees that after cancellation no
// event are called on the callback queue.
guard !task.isCancelling else { return }
switch event {
case .progress(let value): progress?(nil, value)
case .preview(let response): progress?(response, task.currentProgress)
case .cancelled: break // The legacy APIs do not send cancellation events
case .finished(let result): completion(result)
}
}
}
}
}

View File

@ -118,16 +118,6 @@ extension ImagePipeline {
/// `data` schemes) inline without using the data loader. By default, `true`.
public var isLocalResourcesSupportEnabled = true
/// A queue on which all callbacks, like `progress` and `completion`
/// callbacks are called. `.main` by default.
@available(*, deprecated, message: "`ImagePipeline` no longer supports changing the callback queue")
public var callbackQueue: DispatchQueue {
get { _callbackQueue }
set { _callbackQueue = newValue }
}
var _callbackQueue = DispatchQueue.main
// MARK: - Options (Shared)
/// `false` by default. If `true`, enables `os_signpost` logging for
@ -140,7 +130,7 @@ extension ImagePipeline {
set { _isSignpostLoggingEnabled.value = newValue }
}
private static let _isSignpostLoggingEnabled = Atomic(value: false)
private static let _isSignpostLoggingEnabled = Mutex(false)
private var isCustomImageCacheProvided = false
@ -151,10 +141,6 @@ extension ImagePipeline {
/// Data loading queue. Default maximum concurrent task count is 6.
public var dataLoadingQueue = OperationQueue(maxConcurrentCount: 6)
// Deprecated in Nuke 12.6
@available(*, deprecated, message: "The pipeline now performs cache lookup on the internal queue, reducing the amount of context switching")
public var dataCachingQueue = OperationQueue(maxConcurrentCount: 2)
/// Image decoding queue. Default maximum concurrent task count is 1.
public var imageDecodingQueue = OperationQueue(maxConcurrentCount: 1)

View File

@ -8,84 +8,71 @@ import Foundation
///
/// - important: The delegate methods are performed on the pipeline queue in the
/// background.
public protocol ImagePipelineDelegate: AnyObject, Sendable {
// MARK: Configuration
extension ImagePipeline {
public protocol Delegate: AnyObject, Sendable {
// MARK: Configuration
/// Returns data loader for the given request.
func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading
/// Returns data loader for the given request.
func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading
/// Returns image decoder for the given context.
func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)?
/// Returns image decoder for the given context.
func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)?
/// Returns image encoder for the given context.
func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding
/// Returns image encoder for the given context.
func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding
// MARK: Caching
// MARK: Caching
/// Returns in-memory image cache for the given request. Return `nil` to prevent cache reads and writes.
func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)?
/// Returns in-memory image cache for the given request. Return `nil` to prevent cache reads and writes.
func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)?
/// Returns disk cache for the given request. Return `nil` to prevent cache
/// reads and writes.
func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)?
/// Returns disk cache for the given request. Return `nil` to prevent cache
/// reads and writes.
func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)?
/// Returns a cache key identifying the image produced for the given request
/// (including image processors). The key is used for both in-memory and
/// on-disk caches.
///
/// Return `nil` to use a default key.
func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String?
/// Returns a cache key identifying the image produced for the given request
/// (including image processors). The key is used for both in-memory and
/// on-disk caches.
///
/// Return `nil` to use a default key.
func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String?
/// Gets called when the pipeline is about to save data for the given request.
/// The implementation must call the completion closure passing `non-nil` data
/// to enable caching or `nil` to prevent it.
///
/// This method calls only if the request parameters and data caching policy
/// of the pipeline already allow caching.
///
/// - parameters:
/// - data: Either the original data or the encoded image in case of storing
/// a processed or re-encoded image.
/// - image: Non-nil in case storing an encoded image.
/// - request: The request for which image is being stored.
/// - completion: The implementation must call the completion closure
/// passing `non-nil` data to enable caching or `nil` to prevent it. You can
/// safely call it synchronously. The callback gets called on the background
/// thread.
func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void)
/// Gets called when the pipeline is about to save data for the given request.
/// The implementation must call the completion closure passing `non-nil` data
/// to enable caching or `nil` to prevent it.
///
/// This method calls only if the request parameters and data caching policy
/// of the pipeline already allow caching.
///
/// - parameters:
/// - data: Either the original data or the encoded image in case of storing
/// a processed or re-encoded image.
/// - image: Non-nil in case storing an encoded image.
/// - request: The request for which image is being stored.
/// - completion: The implementation must call the completion closure
/// passing `non-nil` data to enable caching or `nil` to prevent it. You can
/// safely call it synchronously. The callback gets called on the background
/// thread.
func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void)
// MARK: Decompression
// MARK: Decompression
func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool
func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool
func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse
func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse
// MARK: ImageTask
// MARK: ImageTask
/// Gets called when the task is created. Unlike other methods, it is called
/// immediately on the caller's queue.
func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline)
/// Gets called when the task is created. Unlike other methods, it is called
/// immediately on the caller's queue.
func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline)
/// Gets called when the task receives an event.
func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline)
/// - warning: Soft-deprecated in Nuke 12.7.
func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline)
/// - warning: Soft-deprecated in Nuke 12.7.
func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline)
/// - warning: Soft-deprecated in Nuke 12.7.
func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline)
/// - warning: Soft-deprecated in Nuke 12.7.
func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline)
/// - warning: Soft-deprecated in Nuke 12.7.
func imageTask(_ task: ImageTask, didCompleteWithResult result: Result<ImageResponse, ImagePipeline.Error>, pipeline: ImagePipeline)
/// Gets called when the task receives an event.
func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline)
}
}
extension ImagePipelineDelegate {
extension ImagePipeline.Delegate {
public func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)? {
pipeline.configuration.imageCache
}
@ -127,16 +114,10 @@ extension ImagePipelineDelegate {
public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {}
public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {}
public func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) {}
public func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) {}
public func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) {}
public func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) {}
public func imageTask(_ task: ImageTask, didCompleteWithResult result: Result<ImageResponse, ImagePipeline.Error>, pipeline: ImagePipeline) {}
}
final class ImagePipelineDefaultDelegate: ImagePipelineDelegate {}
final class ImagePipelineDefaultDelegate: ImagePipeline.Delegate {}
// Deprecated in Nuke 13.0
@available(*, deprecated, renamed: "ImagePipeline.Delegate", message: "")
public typealias ImagePipelineDelegate = ImagePipeline.Delegate

View File

@ -3,7 +3,6 @@
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
import Combine
#if canImport(UIKit)
import UIKit
@ -13,23 +12,24 @@ import UIKit
import AppKit
#endif
/// The pipeline downloads and caches images, and prepares them for display.
public final class ImagePipeline: @unchecked Sendable {
/// The pipeline downloads and caches images, and prepares them for display.
@ImagePipelineActor
public final class ImagePipeline {
/// Returns the shared image pipeline.
public static var shared: ImagePipeline {
public nonisolated static var shared: ImagePipeline {
get { _shared.value }
set { _shared.value = newValue }
}
private static let _shared = Atomic(value: ImagePipeline(configuration: .withURLCache))
private nonisolated static let _shared = Mutex(ImagePipeline(configuration: .withURLCache))
/// The pipeline configuration.
public let configuration: Configuration
public nonisolated let configuration: Configuration
/// Provides access to the underlying caching subsystems.
public var cache: ImagePipeline.Cache { .init(pipeline: self) }
public nonisolated var cache: ImagePipeline.Cache { .init(pipeline: self) }
let delegate: any ImagePipelineDelegate
let delegate: any ImagePipeline.Delegate
private var tasks = [ImageTask: TaskSubscription]()
@ -38,28 +38,17 @@ public final class ImagePipeline: @unchecked Sendable {
private let tasksFetchOriginalImage: TaskPool<TaskFetchOriginalImageKey, ImageResponse, Error>
private let tasksFetchOriginalData: TaskPool<TaskFetchOriginalDataKey, (Data, URLResponse?), Error>
// The queue on which the entire subsystem is synchronized.
let queue = DispatchQueue(label: "com.github.kean.Nuke.ImagePipeline", qos: .userInitiated)
private var isInvalidated = false
private var nextTaskId: Int64 {
os_unfair_lock_lock(lock)
defer { os_unfair_lock_unlock(lock) }
_nextTaskId += 1
return _nextTaskId
}
private var _nextTaskId: Int64 = 0
private let lock: os_unfair_lock_t
private nonisolated let nextTaskId = Mutex<Int64>(0)
let rateLimiter: RateLimiter?
let id = UUID()
var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes
// TODO: remove
nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes
deinit {
lock.deinitialize(count: 1)
lock.deallocate()
ResumableDataStorage.shared.unregister(self)
ResumableDataStorage.shared.unregister(id)
}
/// Initializes the instance with the given configuration.
@ -67,9 +56,9 @@ public final class ImagePipeline: @unchecked Sendable {
/// - parameters:
/// - configuration: The pipeline configuration.
/// - delegate: Provides more ways to customize the pipeline behavior on per-request basis.
public init(configuration: Configuration = Configuration(), delegate: (any ImagePipelineDelegate)? = nil) {
public nonisolated init(configuration: Configuration = Configuration(), delegate: (any ImagePipeline.Delegate)? = nil) {
self.configuration = configuration
self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter(queue: queue) : nil
self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter() : nil
self.delegate = delegate ?? ImagePipelineDefaultDelegate()
(configuration.dataLoader as? DataLoader)?.prefersIncrementalDelivery = configuration.isProgressiveDecodingEnabled
@ -79,10 +68,7 @@ public final class ImagePipeline: @unchecked Sendable {
self.tasksFetchOriginalImage = TaskPool(isCoalescingEnabled)
self.tasksFetchOriginalData = TaskPool(isCoalescingEnabled)
self.lock = .allocate(capacity: 1)
self.lock.initialize(to: os_unfair_lock())
ResumableDataStorage.shared.register(self)
ResumableDataStorage.shared.register(id)
}
/// A convenience way to initialize the pipeline with a closure.
@ -99,7 +85,7 @@ public final class ImagePipeline: @unchecked Sendable {
/// - parameters:
/// - configuration: The pipeline configuration.
/// - delegate: Provides more ways to customize the pipeline behavior on per-request basis.
public convenience init(delegate: (any ImagePipelineDelegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) {
public nonisolated convenience init(delegate: (any ImagePipeline.Delegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) {
var configuration = ImagePipeline.Configuration()
configure(&configuration)
self.init(configuration: configuration, delegate: delegate)
@ -107,28 +93,28 @@ public final class ImagePipeline: @unchecked Sendable {
/// Invalidates the pipeline and cancels all outstanding tasks. Any new
/// requests will immediately fail with ``ImagePipeline/Error/pipelineInvalidated`` error.
public func invalidate() {
queue.async {
public nonisolated func invalidate() {
Task { @ImagePipelineActor in
guard !self.isInvalidated else { return }
self.isInvalidated = true
self.tasks.keys.forEach(self.cancelImageTask)
}
}
// MARK: - Loading Images (Async/Await)
// MARK: - Loading Images
/// Creates a task with the given URL.
///
/// The task starts executing the moment it is created.
public func imageTask(with url: URL) -> ImageTask {
public nonisolated func imageTask(with url: URL) -> ImageTask {
imageTask(with: ImageRequest(url: url))
}
/// Creates a task with the given request.
///
/// The task starts executing the moment it is created.
public func imageTask(with request: ImageRequest) -> ImageTask {
makeStartedImageTask(with: request)
public nonisolated func imageTask(with request: ImageRequest) -> ImageTask {
makeImageTask(with: request)
}
/// Returns an image for the given URL.
@ -141,220 +127,44 @@ public final class ImagePipeline: @unchecked Sendable {
try await imageTask(with: request).image
}
// MARK: - Loading Data (Async/Await)
// MARK: - Loading Data
/// Returns image data for the given request.
///
/// - parameter request: An image request.
public func data(for request: ImageRequest) async throws -> (Data, URLResponse?) {
let task = makeStartedImageTask(with: request, isDataTask: true)
let task = makeImageTask(with: request, isDataTask: true)
let response = try await task.response
return (response.container.data ?? Data(), response.urlResponse)
}
// MARK: - Loading Images (Closures)
/// Loads an image for the given request.
///
/// - parameters:
/// - request: An image request.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public func loadImage(
with url: URL,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: ImageRequest(url: url), progress: nil, completion: completion)
}
/// Loads an image for the given request.
///
/// - parameters:
/// - request: An image request.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public func loadImage(
with request: ImageRequest,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: request, progress: nil, completion: completion)
}
/// Loads an image for the given request.
///
/// - parameters:
/// - request: An image request.
/// - progress: A closure to be called periodically on the main thread when
/// the progress is updated.
/// - completion: A closure to be called on the main thread when the request
/// is finished.
@discardableResult public func loadImage(
with request: ImageRequest,
queue: DispatchQueue? = nil,
progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (_ result: Result<ImageResponse, Error>) -> Void
) -> ImageTask {
_loadImage(with: request, queue: queue, progress: {
progress?($0, $1.completed, $1.total)
}, completion: completion)
}
func _loadImage(
with request: ImageRequest,
isDataTask: Bool = false,
queue callbackQueue: DispatchQueue? = nil,
progress: ((ImageResponse?, ImageTask.Progress) -> Void)?,
completion: @escaping (Result<ImageResponse, Error>) -> Void
) -> ImageTask {
makeStartedImageTask(with: request, isDataTask: isDataTask) { [weak self] event, task in
self?.dispatchCallback(to: callbackQueue) {
// The callback-based API guarantees that after cancellation no
// event are called on the callback queue.
guard task.state != .cancelled else { return }
switch event {
case .progress(let value): progress?(nil, value)
case .preview(let response): progress?(response, task.currentProgress)
case .cancelled: break // The legacy APIs do not send cancellation events
case .finished(let result):
_ = task._setState(.completed) // Important to do it on the callback queue
completion(result)
}
}
}
}
// nuke-13: requires callbacks to be @MainActor @Sendable or deprecate this entire API
private func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) {
let box = UncheckedSendableBox(value: closure)
if callbackQueue === self.queue {
closure()
} else {
(callbackQueue ?? self.configuration._callbackQueue).async {
box.value()
}
}
}
// MARK: - Loading Data (Closures)
/// Loads image data for the given request. The data doesn't get decoded
/// or processed in any other way.
@discardableResult public func loadData(with request: ImageRequest, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask {
_loadData(with: request, queue: nil, progress: nil, completion: completion)
}
private func _loadData(
with request: ImageRequest,
queue: DispatchQueue?,
progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void
) -> ImageTask {
_loadImage(with: request, isDataTask: true, queue: queue) { _, progress in
progressHandler?(progress.completed, progress.total)
} completion: { result in
let result = result.map { response in
// Data should never be empty
(data: response.container.data ?? Data(), response: response.urlResponse)
}
completion(result)
}
}
/// Loads the image data for the given request. The data doesn't get decoded
/// or processed in any other way.
///
/// You can call ``loadImage(with:completion:)-43osv`` for the request at any point after calling
/// ``loadData(with:completion:)-6cwk3``, the pipeline will use the same operation to load the data,
/// no duplicated work will be performed.
///
/// - parameters:
/// - request: An image request.
/// - queue: A queue on which to execute `progress` and `completion`
/// callbacks. By default, the pipeline uses `.main` queue.
/// - progress: A closure to be called periodically on the main thread when the progress is updated.
/// - completion: A closure to be called on the main thread when the request is finished.
@discardableResult public func loadData(
with request: ImageRequest,
queue: DispatchQueue? = nil,
progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?,
completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void
) -> ImageTask {
_loadImage(with: request, isDataTask: true, queue: queue) { _, progress in
progressHandler?(progress.completed, progress.total)
} completion: { result in
let result = result.map { response in
// Data should never be empty
(data: response.container.data ?? Data(), response: response.urlResponse)
}
completion(result)
}
}
// MARK: - Loading Images (Combine)
/// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.
public func imagePublisher(with url: URL) -> AnyPublisher<ImageResponse, Error> {
imagePublisher(with: ImageRequest(url: url))
}
/// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.
public func imagePublisher(with request: ImageRequest) -> AnyPublisher<ImageResponse, Error> {
ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher()
}
// MARK: - ImageTask (Internal)
private func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask {
let task = ImageTask(taskId: nextTaskId, request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent)
// Important to call it before `imageTaskStartCalled`
if !isDataTask {
delegate.imageTaskCreated(task, pipeline: self)
}
task._task = Task {
try await withUnsafeThrowingContinuation { continuation in
self.queue.async {
task._continuation = continuation
self.startImageTask(task, isDataTask: isDataTask)
}
}
}
nonisolated func makeImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask {
let task = ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent)
delegate.imageTaskCreated(task, pipeline: self)
return task
}
// By this time, the task has `continuation` set and is fully wired.
private func startImageTask(_ task: ImageTask, isDataTask: Bool) {
guard task._state != .cancelled else {
// The task gets started asynchronously in a `Task` and cancellation
// can happen before the pipeline reached `startImageTask`. In that
// case, the `cancel` method do no send the task event.
return task._dispatch(.cancelled)
}
func startImageTask(_ task: ImageTask, isDataTask: Bool) {
guard !isInvalidated else {
return task._process(.error(.pipelineInvalidated))
return task.process(.error(.pipelineInvalidated))
}
let worker = isDataTask ? makeTaskLoadData(for: task.request) : makeTaskLoadImage(for: task.request)
tasks[task] = worker.subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak task] in
task?._process($0)
task?.process($0)
}
delegate.imageTaskDidStart(task, pipeline: self)
onTaskStarted?(task)
}
private func cancelImageTask(_ task: ImageTask) {
func cancelImageTask(_ task: ImageTask) {
tasks.removeValue(forKey: task)?.unsubscribe()
task._cancel()
}
// MARK: - Image Task Events
func imageTaskCancelCalled(_ task: ImageTask) {
queue.async { self.cancelImageTask(task) }
}
func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) {
queue.async {
self.tasks[task]?.setPriority(priority.taskPriority)
}
func imageTask(_ task: ImageTask, didChangePriority priority: ImageRequest.Priority) {
self.tasks[task]?.setPriority(priority.taskPriority)
}
func imageTask(_ task: ImageTask, didProcessEvent event: ImageTask.Event, isDataTask: Bool) {
@ -363,19 +173,8 @@ public final class ImagePipeline: @unchecked Sendable {
tasks[task] = nil
default: break
}
if !isDataTask {
delegate.imageTask(task, didReceiveEvent: event, pipeline: self)
switch event {
case .progress(let progress):
delegate.imageTask(task, didUpdateProgress: progress, pipeline: self)
case .preview(let response):
delegate.imageTask(task, didReceivePreview: response, pipeline: self)
case .cancelled:
delegate.imageTaskDidCancel(task, pipeline: self)
case .finished(let result):
delegate.imageTask(task, didCompleteWithResult: result, pipeline: self)
}
}
}
@ -419,21 +218,7 @@ public final class ImagePipeline: @unchecked Sendable {
func makeTaskFetchOriginalData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher {
tasksFetchOriginalData.publisherForKey(TaskFetchOriginalDataKey(request)) {
request.publisher == nil ? TaskFetchOriginalData(self, request) : TaskFetchWithPublisher(self, request)
request.closure == nil ? TaskFetchOriginalData(self, request) : TaskFetchWithClosure(self, request)
}
}
// MARK: - Deprecated
// Deprecated in Nuke 12.7
@available(*, deprecated, message: "Please the variant variant that accepts `ImageRequest` as a parameter")
@discardableResult public func loadData(with url: URL, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask {
loadData(with: ImageRequest(url: url), queue: nil, progress: nil, completion: completion)
}
// Deprecated in Nuke 12.7
@available(*, deprecated, message: "Please the variant that accepts `ImageRequest` as a parameter")
@discardableResult public func data(for url: URL) async throws -> (Data, URLResponse?) {
try await data(for: ImageRequest(url: url))
}
}

View File

@ -0,0 +1,13 @@
// The MIT License (MIT)
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
// swiftlint:disable convenience_type
@globalActor
public struct ImagePipelineActor {
public actor ImagePipelineActor { }
public static let shared = ImagePipelineActor()
}
// swiftlint:enable convenience_type

View File

@ -11,23 +11,32 @@ import Foundation
///
/// All ``ImagePrefetcher`` methods are thread-safe and are optimized to be used
/// even from the main thread during scrolling.
public final class ImagePrefetcher: @unchecked Sendable {
/// Pauses the prefetching.
@ImagePipelineActor
public final class ImagePrefetcher {
/// Pauses the prefetching.
///
/// - note: When you pause, the prefetcher will finish outstanding tasks
/// (by default, there are only 2 at a time), and pause the rest.
public var isPaused: Bool = false {
didSet { queue.isSuspended = isPaused }
public nonisolated var isPaused: Bool {
get { queue.isSuspended }
set { queue.isSuspended = newValue }
}
/// The priority of the requests. By default, ``ImageRequest/Priority-swift.enum/low``.
///
/// Changing the priority also changes the priority of all of the outstanding
/// tasks managed by the prefetcher.
public var priority: ImageRequest.Priority = .low {
didSet {
let newValue = priority
pipeline.queue.async { self.didUpdatePriority(to: newValue) }
public nonisolated var priority: ImageRequest.Priority {
get { _priority.value }
set {
guard _priority.withLock({
guard $0 != newValue else { return false }
$0 = newValue
return true
}) else { return }
Task {
await didUpdatePriority(to: newValue)
}
}
}
@ -48,15 +57,15 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// The closure that gets called when the prefetching completes for all the
/// scheduled requests. The closure is always called on completion,
/// regardless of whether the requests succeed or some fail.
///
/// - note: The closure is called on the main queue.
public var didComplete: (@MainActor @Sendable () -> Void)?
private let pipeline: ImagePipeline
private var tasks = [TaskLoadImageKey: Task]()
private let destination: Destination
private var _priority: ImageRequest.Priority = .low
let queue = OperationQueue() // internal for testing
private var tasks = [TaskLoadImageKey: PrefetchTask]()
private let _priority = Mutex(ImageRequest.Priority.low)
// internal for testing
nonisolated let didComplete = Mutex<(@Sendable () -> Void)?>(nil)
nonisolated let queue = OperationQueue()
/// Initializes the ``ImagePrefetcher`` instance.
///
@ -64,20 +73,21 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// - pipeline: The pipeline used for loading images.
/// - destination: By default load images in all cache layers.
/// - maxConcurrentRequestCount: 2 by default.
public init(pipeline: ImagePipeline = ImagePipeline.shared,
destination: Destination = .memoryCache,
maxConcurrentRequestCount: Int = 2) {
public nonisolated init(
pipeline: ImagePipeline = ImagePipeline.shared,
destination: Destination = .memoryCache,
maxConcurrentRequestCount: Int = 2
) {
self.pipeline = pipeline
self.destination = destination
self.queue.maxConcurrentOperationCount = maxConcurrentRequestCount
self.queue.underlyingQueue = pipeline.queue
}
deinit {
let tasks = self.tasks.values // Make sure we don't retain self
self.tasks.removeAll()
pipeline.queue.async {
Task { @ImagePipelineActor in
for task in tasks {
task.cancel()
}
@ -87,7 +97,7 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// Starts prefetching images for the given URL.
///
/// See also ``startPrefetching(with:)-718dg`` that works with ``ImageRequest``.
public func startPrefetching(with urls: [URL]) {
public nonisolated func startPrefetching(with urls: [URL]) {
startPrefetching(with: urls.map { ImageRequest(url: $0) })
}
@ -101,17 +111,18 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// (`.low` by default).
///
/// See also ``startPrefetching(with:)-1jef2`` that works with `URL`.
public func startPrefetching(with requests: [ImageRequest]) {
pipeline.queue.async {
public nonisolated func startPrefetching(with requests: [ImageRequest]) {
Task { @ImagePipelineActor in
self._startPrefetching(with: requests)
}
}
public func _startPrefetching(with requests: [ImageRequest]) {
private func _startPrefetching(with requests: [ImageRequest]) {
let priority = _priority.value
for request in requests {
var request = request
if _priority != request.priority {
request.priority = _priority
if priority != request.priority {
request.priority = priority
}
_startPrefetching(with: request)
}
@ -126,41 +137,43 @@ public final class ImagePrefetcher: @unchecked Sendable {
guard tasks[key] == nil else {
return
}
let task = Task(request: request, key: key)
let task = PrefetchTask(request: request, key: key)
task.operation = queue.add { [weak self] finish in
guard let self else { return finish() }
self.loadImage(task: task, finish: finish)
Task { @ImagePipelineActor in
self.loadImage(task: task, finish: finish)
}
}
tasks[key] = task
return
}
private func loadImage(task: Task, finish: @escaping () -> Void) {
task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, queue: pipeline.queue, progress: nil) { [weak self] _ in
private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) {
let imageTask = pipeline.makeImageTask(with: task.request, isDataTask: destination == .diskCache)
task.imageTask = imageTask
Task { [weak self] in
_ = try? await imageTask.response
self?._remove(task)
finish()
}
task.onCancelled = finish
}
private func _remove(_ task: Task) {
private func _remove(_ task: PrefetchTask) {
guard tasks[task.key] === task else { return } // Should never happen
tasks[task.key] = nil
sendCompletionIfNeeded()
}
private func sendCompletionIfNeeded() {
guard tasks.isEmpty, let callback = didComplete else {
return
}
DispatchQueue.main.async(execute: callback)
if tasks.isEmpty { didComplete.value?() }
}
/// Stops prefetching images for the given URLs and cancels outstanding
/// requests.
///
/// See also ``stopPrefetching(with:)-8cdam`` that works with ``ImageRequest``.
public func stopPrefetching(with urls: [URL]) {
public nonisolated func stopPrefetching(with urls: [URL]) {
stopPrefetching(with: urls.map { ImageRequest(url: $0) })
}
@ -172,8 +185,8 @@ public final class ImagePrefetcher: @unchecked Sendable {
/// of ``ImagePrefetcher``.
///
/// See also ``stopPrefetching(with:)-2tcyq`` that works with `URL`.
public func stopPrefetching(with requests: [ImageRequest]) {
pipeline.queue.async {
public nonisolated func stopPrefetching(with requests: [ImageRequest]) {
Task { @ImagePipelineActor in
for request in requests {
self._stopPrefetching(with: request)
}
@ -187,22 +200,20 @@ public final class ImagePrefetcher: @unchecked Sendable {
}
/// Stops all prefetching tasks.
public func stopPrefetching() {
pipeline.queue.async {
public nonisolated func stopPrefetching() {
Task { @ImagePipelineActor in
self.tasks.values.forEach { $0.cancel() }
self.tasks.removeAll()
}
}
private func didUpdatePriority(to priority: ImageRequest.Priority) {
guard _priority != priority else { return }
_priority = priority
for task in tasks.values {
task.imageTask?.priority = priority
}
}
private final class Task: @unchecked Sendable {
private final class PrefetchTask: @unchecked Sendable {
let key: TaskLoadImageKey
let request: ImageRequest
weak var imageTask: ImageTask?

View File

@ -34,7 +34,7 @@ public enum ImageProcessingOptions: Sendable {
/// views in which they get displayed. If you can't guarantee that, pleasee
/// consider adding border to a view layer. This should be your primary
/// option regardless.
public struct Border: Hashable, CustomStringConvertible, @unchecked Sendable {
public struct Border: Hashable, CustomStringConvertible, Sendable {
public let width: CGFloat
#if canImport(UIKit)

View File

@ -83,7 +83,7 @@ extension ImageProcessors {
set { _context.value = newValue }
}
private static let _context = Atomic(value: CIContext(options: [.priorityRequestLow: true]))
private static let _context = Mutex(CIContext(options: [.priorityRequestLow: true]))
static func applyFilter(named name: String, parameters: [String: Any] = [:], to image: PlatformImage) throws -> PlatformImage {
guard let filter = CIFilter(name: name, parameters: parameters) else {

View File

@ -19,10 +19,6 @@ extension ImageProcessors {
private let crop: Bool
private let upscale: Bool
// Deprecated in Nuke 12.0
@available(*, deprecated, message: "Renamed to `ImageProcessingOptions.ContentMode")
public typealias ContentMode = ImageProcessingOptions.ContentMode
/// Initializes the processor with the given size.
///
/// - parameters:

View File

@ -6,7 +6,7 @@ import Foundation
// Each task holds a strong reference to the pipeline. This is by design. The
// user does not need to hold a strong reference to the pipeline.
class AsyncPipelineTask<Value: Sendable>: AsyncTask<Value, ImagePipeline.Error>, @unchecked Sendable {
class AsyncPipelineTask<Value: Sendable>: AsyncTask<Value, ImagePipeline.Error> {
let pipeline: ImagePipeline
// A canonical request representing the unit work performed by the task.
let request: ImageRequest
@ -19,6 +19,7 @@ class AsyncPipelineTask<Value: Sendable>: AsyncTask<Value, ImagePipeline.Error>,
// Returns all image tasks subscribed to the current pipeline task.
// A suboptimal approach just to make the new DiskCachPolicy.automatic work.
@ImagePipelineActor
protocol ImageTaskSubscribers {
var imageTasks: [ImageTask] { get }
}
@ -50,12 +51,9 @@ extension AsyncPipelineTask {
guard decoder.isAsynchronous else {
return completion(decode())
}
operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in
guard let self else { return }
operation = pipeline.configuration.imageDecodingQueue.add {
let response = decode()
self.pipeline.queue.async {
completion(response)
}
completion(response)
}
}
}

View File

@ -14,9 +14,8 @@ import Foundation
/// automatically cancels them, updates the priority, etc. Most steps in the
/// image pipeline are represented using Operation to take advantage of these features.
///
/// - warning: Must be thread-confined!
class AsyncTask<Value: Sendable, Error: Sendable>: AsyncTaskSubscriptionDelegate, @unchecked Sendable {
@ImagePipelineActor
class AsyncTask<Value: Sendable, Error: Sendable>: AsyncTaskSubscriptionDelegate {
private struct Subscription {
let closure: (Event) -> Void
weak var subscriber: AnyObject?
@ -76,6 +75,8 @@ class AsyncTask<Value: Sendable, Error: Sendable>: AsyncTaskSubscriptionDelegate
/// Override this to start image task. Only gets called once.
func start() {}
init() {}
// MARK: - Managing Observers
/// - notes: Returns `nil` if the task was disposed.
@ -218,6 +219,7 @@ class AsyncTask<Value: Sendable, Error: Sendable>: AsyncTaskSubscriptionDelegate
extension AsyncTask {
/// Publishes the results of the task.
@ImagePipelineActor
struct Publisher {
fileprivate let task: AsyncTask
@ -281,7 +283,8 @@ extension AsyncTask.Event: Equatable where Value: Equatable, Error: Equatable {}
/// Represents a subscription to a task. The observer must retain a strong
/// reference to a subscription.
struct TaskSubscription: Sendable {
@ImagePipelineActor
struct TaskSubscription {
private let task: any AsyncTaskSubscriptionDelegate
private let key: TaskSubscriptionKey
@ -311,7 +314,8 @@ struct TaskSubscription: Sendable {
}
}
private protocol AsyncTaskSubscriptionDelegate: AnyObject, Sendable {
@ImagePipelineActor
private protocol AsyncTaskSubscriptionDelegate: AnyObject {
func unsubsribe(key: TaskSubscriptionKey)
func setPriority(_ priority: TaskPriority, for observer: TaskSubscriptionKey)
}
@ -320,12 +324,12 @@ private typealias TaskSubscriptionKey = Int
// MARK: - TaskPool
/// Contains the tasks which haven't completed yet.
@ImagePipelineActor
final class TaskPool<Key: Hashable, Value: Sendable, Error: Sendable> {
private let isCoalescingEnabled: Bool
private var map = [Key: AsyncTask<Value, Error>]()
init(_ isCoalescingEnabled: Bool) {
nonisolated init(_ isCoalescingEnabled: Bool) {
self.isCoalescingEnabled = isCoalescingEnabled
}

View File

@ -6,7 +6,7 @@ import Foundation
/// Fetches original image from the data loader (`DataLoading`) and stores it
/// in the disk cache (`DataCaching`).
final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable {
final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> {
private var urlResponse: URLResponse?
private var resumableData: ResumableData?
private var resumedDataCount: Int64 = 0
@ -54,7 +54,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc
guard let self else {
return finish()
}
self.pipeline.queue.async {
Task { @ImagePipelineActor in
self.loadData(urlRequest: urlRequest, finish: finish)
}
}
@ -70,7 +70,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc
// back in the cache if the request fails to complete again).
var urlRequest = urlRequest
if pipeline.configuration.isResumableDataEnabled,
let resumableData = ResumableDataStorage.shared.removeResumableData(for: request, pipeline: pipeline) {
let resumableData = ResumableDataStorage.shared.removeResumableData(for: request, namespace: pipeline.id) {
// Update headers to add "Range" and "If-Range" headers
resumableData.resume(request: &urlRequest)
// Save resumable data to be used later (before using it, the pipeline
@ -78,28 +78,28 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc
self.resumableData = resumableData
}
signpost(self, "LoadImageData", .begin, "URL: \(urlRequest.url?.absoluteString ?? ""), resumable data: \(Formatter.bytes(resumableData?.data.count ?? 0))")
signpost(self, "LoadImageData", .begin, "URL: \(String(describing: urlRequest.url))")
let dataLoader = pipeline.delegate.dataLoader(for: request, pipeline: pipeline)
let dataTask = dataLoader.loadData(with: urlRequest, didReceiveData: { [weak self] data, response in
guard let self else { return }
self.pipeline.queue.async {
self.dataTask(didReceiveData: data, response: response)
let task = Task { @ImagePipelineActor in
do {
for try await (data, response) in dataLoader.loadData(for: urlRequest) {
dataTask(didReceiveData: data, response: response)
}
dataTaskDidFinish(error: nil)
} catch {
dataTaskDidFinish(error: error)
}
}, completion: { [weak self] error in
finish() // Finish the operation!
guard let self else { return }
signpost(self, "LoadImageData", .end, "Finished with size \(Formatter.bytes(self.data.count))")
self.pipeline.queue.async {
self.dataTaskDidFinish(error: error)
}
})
finish() // Finish the operation!
}
onCancelled = { [weak self] in
guard let self else { return }
signpost(self, "LoadImageData", .end, "Cancelled")
dataTask.cancel()
task.cancel()
finish() // Finish the operation!
self.tryToSaveResumableData()
@ -162,7 +162,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc
if pipeline.configuration.isResumableDataEnabled,
let response = urlResponse, !data.isEmpty,
let resumableData = ResumableData(response: response, data: data) {
ResumableDataStorage.shared.storeResumableData(resumableData, for: request, pipeline: pipeline)
ResumableDataStorage.shared.storeResumableData(resumableData, for: request, namespace: pipeline.id)
}
}
}

View File

@ -5,7 +5,7 @@
import Foundation
/// Receives data from ``TaskLoadImageData`` and decodes it as it arrives.
final class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {
final class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse> {
private var decoder: (any ImageDecoding)?
override func start() {
@ -37,9 +37,11 @@ final class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse>, @unchecked
}
return
}
decode(context, decoder: decoder) { [weak self] in
self?.didFinishDecoding(context: context, result: $0)
decode(context, decoder: decoder) { [weak self] result in
guard let self else { return }
Task {
await self.didFinishDecoding(context: context, result: result)
}
}
}

View File

@ -6,7 +6,7 @@ import Foundation
/// Fetches data using the publisher provided with the request.
/// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved.
final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable {
final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> {
private lazy var data = Data()
override func start() {
@ -19,7 +19,7 @@ final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @un
guard let self else {
return finish()
}
self.pipeline.queue.async {
Task { @ImagePipelineActor in
self.loadData { finish() }
}
}
@ -32,41 +32,27 @@ final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @un
return finish()
}
guard let publisher = request.publisher else {
guard let closure = request.closure else {
send(error: .dataLoadingFailed(error: URLError(.unknown))) // This is just a placeholder error, never thrown
return assertionFailure("This should never happen")
}
let cancellable = publisher.sink(receiveCompletion: { [weak self] result in
let task = Task { @ImagePipelineActor in
do {
let data = try await closure()
guard !data.isEmpty else {
throw ImagePipeline.Error.dataIsEmpty
}
storeDataInCacheIfNeeded(data)
send(value: (data, nil), isCompleted: true)
} catch {
send(error: .dataLoadingFailed(error: error))
}
finish() // Finish the operation!
guard let self else { return }
self.pipeline.queue.async {
self.dataTaskDidFinish(result)
}
}, receiveValue: { [weak self] data in
guard let self else { return }
self.pipeline.queue.async {
self.data.append(data)
}
})
}
onCancelled = {
finish()
cancellable.cancel()
}
}
private func dataTaskDidFinish(_ result: PublisherCompletion) {
switch result {
case .finished:
guard !data.isEmpty else {
send(error: .dataIsEmpty)
return
}
storeDataInCacheIfNeeded(data)
send(value: (data, nil), isCompleted: true)
case .failure(let error):
send(error: .dataLoadingFailed(error: error))
task.cancel()
}
}
}

View File

@ -5,7 +5,7 @@
import Foundation
/// Wrapper for tasks created by `loadData` calls.
final class TaskLoadData: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {
final class TaskLoadData: AsyncPipelineTask<ImageResponse> {
override func start() {
if let data = pipeline.cache.cachedData(for: request) {
let container = ImageContainer(image: .init(), data: data)

View File

@ -9,7 +9,7 @@ import Foundation
/// Performs all the quick cache lookups and also manages image processing.
/// The coalescing for image processing is implemented on demand (extends the
/// scenarios in which coalescing can kick in).
final class TaskLoadImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {
final class TaskLoadImage: AsyncPipelineTask<ImageResponse> {
override func start() {
if let container = pipeline.cache[request] {
let response = ImageResponse(container: container, request: request, cacheType: .memory)
@ -30,8 +30,11 @@ final class TaskLoadImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable
guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else {
return didFinishDecoding(with: nil)
}
decode(context, decoder: decoder) { [weak self] in
self?.didFinishDecoding(with: try? $0.get())
decode(context, decoder: decoder) { [weak self] result in
guard let self else { return }
Task {
await self.didFinishDecoding(with: try? result.get())
}
}
}
@ -82,7 +85,7 @@ final class TaskLoadImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable
ImagePipeline.Error.processingFailed(processor: processor, context: context, error: error)
}
}
self.pipeline.queue.async {
Task { @ImagePipelineActor in
self.operation = nil
self.didFinishProcessing(result: result, isCompleted: isCompleted)
}
@ -117,7 +120,7 @@ final class TaskLoadImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable
let response = signpost(isCompleted ? "DecompressImage" : "DecompressProgressiveImage") {
self.pipeline.delegate.decompress(response: response, request: self.request, pipeline: self.pipeline)
}
self.pipeline.queue.async {
Task { @ImagePipelineActor in
self.operation = nil
self.didReceiveDecompressedImage(response, isCompleted: isCompleted)
}

View File

@ -1,9 +1,22 @@
// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import Foundation
import Combine
import Foundation
import Nuke
extension ImagePipeline {
/// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.
public nonisolated func imagePublisher(with url: URL) -> AnyPublisher<ImageResponse, Error> {
imagePublisher(with: ImageRequest(url: url))
}
/// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added.
public nonisolated func imagePublisher(with request: ImageRequest) -> AnyPublisher<ImageResponse, Error> {
ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher()
}
}
/// A publisher that starts a new `ImageTask` when a subscriber is added.
///

View File

@ -290,7 +290,7 @@ private final class ImageViewController {
imageView.nuke_display(image: nil, data: nil) // Remove previously displayed images (if any)
}
task = pipeline.loadImage(with: request, queue: .main, progress: { [weak self] response, completedCount, totalCount in
task = pipeline.loadImage(with: request, progress: { [weak self] response, completedCount, totalCount in
if let response, options.isProgressiveRenderingEnabled {
self?.handle(partialImage: response)
}

View File

@ -194,6 +194,8 @@ public final class FetchImage: ObservableObject, Identifiable {
// MARK: Load (Combine)
// TODO: (nuke13) deprecate these
/// Loads an image with the given publisher.
///
/// - important: Some `FetchImage` features, such as progress reporting and

View File

@ -290,22 +290,18 @@ public final class LazyImageView: _PlatformBaseView {
setPlaceholderViewHidden(false)
let task = pipeline.loadImage(
with: request,
queue: .main,
progress: { [weak self] response, completed, total in
guard let self else { return }
let progress = ImageTask.Progress(completed: completed, total: total)
if let response {
self.handle(preview: response)
self.onPreview?(response)
} else {
self.onProgress?(progress)
}
},
completion: { [weak self] result in
self?.handle(result: result.mapError { $0 }, isSync: false)
let task = pipeline.loadImage(with: request, progress: { [weak self] response, completed, total in
guard let self else { return }
let progress = ImageTask.Progress(completed: completed, total: total)
if let response {
self.handle(preview: response)
self.onPreview?(response)
} else {
self.onProgress?(progress)
}
}, completion: { [weak self] result in
self?.handle(result: result.mapError { $0 }, isSync: false)
}
)
imageTask = task
onStart?(task)

View File

@ -2,12 +2,7 @@
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
#if swift(>=6.0)
import AVKit
#else
@preconcurrency import AVKit
#endif
import Foundation
#if os(macOS)

View File

@ -169,7 +169,7 @@ extension Result {
}
}
@propertyWrapper final class Atomic<T> {
@propertyWrapper final class Mutex<T> {
private var value: T
private let lock: os_unfair_lock_t

View File

@ -5,82 +5,57 @@
import XCTest
@testable import Nuke
final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable {
var startedTaskCount = 0
final class ImagePipelineObserver: ImagePipeline.Delegate, @unchecked Sendable {
var createdTaskCount = 0
var cancelledTaskCount = 0
var completedTaskCount = 0
static let didStartTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidStartTask")
static let didCreateTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.didCreateTask")
static let didCancelTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidCancelTask")
static let didCompleteTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidFinishTask")
static let taskKey = "taskKey"
static let resultKey = "resultKey"
var events = [ImageTaskEvent]()
var onTaskCreated: ((ImageTask) -> Void)?
var events = [ImageTask.Event]()
private let lock = NSLock()
private func append(_ event: ImageTaskEvent) {
private func append(_ event: ImageTask.Event) {
lock.lock()
events.append(event)
lock.unlock()
}
func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {
onTaskCreated?(task)
append(.created)
createdTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didCreateTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])
}
func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) {
startedTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])
append(.started)
}
func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {
append(event)
func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) {
append(.cancelled)
cancelledTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])
}
func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) {
append(.progressUpdated(completedUnitCount: progress.completed, totalUnitCount: progress.total))
}
func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) {
append(.intermediateResponseReceived(response: response))
}
func imageTask(_ task: ImageTask, didCompleteWithResult result: Result<ImageResponse, ImagePipeline.Error>, pipeline: ImagePipeline) {
append(.completed(result: result))
completedTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result])
}
}
enum ImageTaskEvent: Equatable {
case created
case started
case cancelled
case intermediateResponseReceived(response: ImageResponse)
case progressUpdated(completedUnitCount: Int64, totalUnitCount: Int64)
case completed(result: Result<ImageResponse, ImagePipeline.Error>)
static func == (lhs: ImageTaskEvent, rhs: ImageTaskEvent) -> Bool {
switch (lhs, rhs) {
case (.created, .created): return true
case (.started, .started): return true
case (.cancelled, .cancelled): return true
case let (.intermediateResponseReceived(lhs), .intermediateResponseReceived(rhs)): return lhs == rhs
case let (.progressUpdated(lhsTotal, lhsCompleted), .progressUpdated(rhsTotal, rhsCompleted)):
return (lhsTotal, lhsCompleted) == (rhsTotal, rhsCompleted)
case let (.completed(lhs), .completed(rhs)): return lhs == rhs
default: return false
switch event {
case .finished(let result):
completedTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result])
case .cancelled:
cancelledTaskCount += 1
NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task])
default:
break
}
}
}
extension ImageTask.Event: @retroactive Equatable {
public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool {
switch (lhs, rhs) {
case let (.progress(lhs), .progress(rhs)): lhs == rhs
case let (.preview(lhs), .preview(rhs)): lhs == rhs
case let (.finished(lhs), .finished(rhs)): lhs == rhs
case (.cancelled, .cancelled): true
default: false
}
}
}

View File

@ -7,18 +7,18 @@ import Nuke
private let data: Data = Test.data(name: "fixture", extension: "jpeg")
private final class MockDataTask: Cancellable, @unchecked Sendable {
private final class MockDataTask: MockDataTaskProtocol, @unchecked Sendable {
var _cancel: () -> Void = { }
func cancel() {
_cancel()
}
}
class MockDataLoader: DataLoading, @unchecked Sendable {
class MockDataLoader: MockDataLoading, DataLoading, @unchecked Sendable {
static let DidStartTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidStartTask")
static let DidCancelTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidCancelTask")
@Atomic var createdTaskCount = 0
@Mutex var createdTaskCount = 0
var results = [URL: Result<(Data, URLResponse), NSError>]()
let queue = OperationQueue()
var isSuspended: Bool {
@ -26,7 +26,7 @@ class MockDataLoader: DataLoading, @unchecked Sendable {
set { queue.isSuspended = newValue }
}
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable {
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol {
let task = MockDataTask()
NotificationCenter.default.post(name: MockDataLoader.DidStartTask, object: self)
@ -61,3 +61,31 @@ class MockDataLoader: DataLoading, @unchecked Sendable {
return task
}
}
// Remove these and update to implement the actual protocol.
protocol MockDataLoading: DataLoading {
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol
}
extension MockDataLoading where Self: DataLoading {
func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> {
AsyncThrowingStream { continuation in
let task = loadData(with: request) { data, response in
continuation.yield((data, response))
} completion: { error in
continuation.finish(throwing: error)
}
continuation.onTermination = { reason in
switch reason {
case .cancelled: task.cancel()
default: break
}
}
}
}
}
protocol MockDataTaskProtocol {
func cancel()
}

View File

@ -7,12 +7,12 @@ import Nuke
// One-shot data loader that servers data split into chunks, only send one chunk
// per one `resume()` call.
final class MockProgressiveDataLoader: DataLoading, @unchecked Sendable {
final class MockProgressiveDataLoader: MockDataLoading, DataLoading, @unchecked Sendable {
let urlResponse: HTTPURLResponse
var chunks: [Data]
let data = Test.data(name: "progressive", extension: "jpeg")
class _MockTask: Cancellable, @unchecked Sendable {
class _MockTask: MockDataTaskProtocol, @unchecked Sendable {
func cancel() {
// Do nothing
}
@ -26,7 +26,7 @@ final class MockProgressiveDataLoader: DataLoading, @unchecked Sendable {
self.chunks = Array(_createChunks(for: data, size: data.count / 3))
}
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable {
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol {
self.didReceiveData = didReceiveData
self.completion = completion
self.resume()

View File

@ -29,7 +29,7 @@ extension ImageResponse: Equatable {
}
extension ImagePipeline {
func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline {
nonisolated func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline {
var configuration = self.configuration
configure(&configuration)
return ImagePipeline(configuration: configuration)
@ -37,13 +37,16 @@ extension ImagePipeline {
}
extension ImagePipeline {
@MainActor
private static var stack = [ImagePipeline]()
@MainActor
static func pushShared(_ shared: ImagePipeline) {
stack.append(ImagePipeline.shared)
ImagePipeline.shared = shared
}
@MainActor
static func popShared() {
ImagePipeline.shared = stack.removeLast()
}

View File

@ -27,43 +27,6 @@ class ImagePipelinePublisherTests: XCTestCase {
}
}
func testLoadWithPublisher() throws {
// GIVEN
let request = ImageRequest(id: "a", dataPublisher: Just(Test.data))
// WHEN
let record = expect(pipeline).toLoadImage(with: request)
wait()
// THEN
let image = try XCTUnwrap(record.image)
XCTAssertEqual(image.sizeInPixels, CGSize(width: 640, height: 480))
}
func testLoadWithPublisherAndApplyProcessor() throws {
// GIVEN
var request = ImageRequest(id: "a", dataPublisher: Just(Test.data))
request.processors = [MockImageProcessor(id: "1")]
// WHEN
let record = expect(pipeline).toLoadImage(with: request)
wait()
// THEN
let image = try XCTUnwrap(record.image)
XCTAssertEqual(image.sizeInPixels, CGSize(width: 640, height: 480))
XCTAssertEqual(image.nk_test_processorIDs, ["1"])
}
func testImageRequestWithPublisher() {
// GIVEN
let request = ImageRequest(id: "a", dataPublisher: Just(Test.data))
// THEN
XCTAssertNil(request.urlRequest)
XCTAssertNil(request.url)
}
func testCancellation() {
// GIVEN
dataLoader.isSuspended = true
@ -77,19 +40,6 @@ class ImagePipelinePublisherTests: XCTestCase {
wait()
}
func testDataIsStoredInDataCache() {
// GIVEN
let request = ImageRequest(id: "a", dataPublisher: Just(Test.data))
// WHEN
expect(pipeline).toLoadImage(with: request)
// THEN
wait { _ in
XCTAssertFalse(self.dataCache.store.isEmpty)
}
}
func testInitWithURL() {
_ = pipeline.imagePublisher(with: URL(string: "https://example.com/image.jpeg")!)
}

View File

@ -35,7 +35,8 @@ class ImageViewExtensionsTests: XCTestCase {
imageView = _ImageView()
}
@MainActor
override func tearDown() {
super.tearDown()
@ -270,7 +271,7 @@ class ImageViewExtensionsTests: XCTestCase {
dataLoader.isSuspended = true
// Given an image view with an associated image task
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
expectNotification(ImagePipelineObserver.didCreateTask, object: observer)
NukeExtensions.loadImage(with: Test.url, into: imageView)
wait()
@ -287,7 +288,7 @@ class ImageViewExtensionsTests: XCTestCase {
dataLoader.isSuspended = true
// Given an image view with an associated image task
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
expectNotification(ImagePipelineObserver.didCreateTask, object: observer)
NukeExtensions.loadImage(with: Test.url, into: imageView)
wait()
@ -307,7 +308,7 @@ class ImageViewExtensionsTests: XCTestCase {
autoreleasepool {
// Given an image view with an associated image task
var imageView: _ImageView! = _ImageView()
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
expectNotification(ImagePipelineObserver.didCreateTask, object: observer)
NukeExtensions.loadImage(with: Test.url, into: imageView)
wait()

View File

@ -26,7 +26,8 @@ class ImageViewIntegrationTests: XCTestCase {
imageView = _ImageView()
}
@MainActor
override func tearDown() {
super.tearDown()

View File

@ -28,7 +28,8 @@ class ImageViewLoadingOptionsTests: XCTestCase {
imageView = _ImageView()
}
@MainActor
override func tearDown() {
super.tearDown()

View File

@ -1,40 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
import Combine
@testable import Nuke
internal final class DataPublisherTests: XCTestCase {
private var cancellable: (any Nuke.Cancellable)?
func testInitNotStartsExecutionRightAway() {
let operation = MockOperation()
let publisher = DataPublisher(id: UUID().uuidString) {
await operation.execute()
}
XCTAssertEqual(0, operation.executeCalls)
let expOp = expectation(description: "Waits for MockOperation to complete execution")
cancellable = publisher.sink { completion in expOp.fulfill() } receiveValue: { _ in }
wait(for: [expOp], timeout: 0.2)
XCTAssertEqual(1, operation.executeCalls)
}
private final class MockOperation: @unchecked Sendable {
private(set) var executeCalls = 0
func execute() async -> Data {
executeCalls += 1
await Task.yield()
return Data()
}
}
}

View File

@ -117,7 +117,7 @@ private func checkAccessCachedImages07() {
_ = pipeline.cache.makeDataCacheKey(for: request)
}
private final class CheckAccessCachedImages08: ImagePipelineDelegate {
private final class CheckAccessCachedImages08: ImagePipeline.Delegate {
func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {
request.userInfo["imageId"] as? String
}

View File

@ -15,8 +15,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable {
private var recordedPreviews: [ImageResponse] = []
private var pipelineDelegate = ImagePipelineObserver()
private var imageTask: ImageTask?
private let callbackQueue = DispatchQueue(label: "testChangingCallbackQueue")
private let callbackQueueKey = DispatchSpecificKey<Void>()
override func setUp() {
super.setUp()
@ -25,10 +23,7 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable {
pipeline = ImagePipeline(delegate: pipelineDelegate) {
$0.dataLoader = dataLoader
$0.imageCache = nil
$0._callbackQueue = callbackQueue
}
callbackQueue.setSpecific(key: callbackQueueKey, value: ())
}
// MARK: - Basics
@ -89,23 +84,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable {
XCTAssertTrue(caughtError is CancellationError)
}
func testCancelFromTaskCreated() async throws {
dataLoader.queue.isSuspended = true
pipelineDelegate.onTaskCreated = { $0.cancel() }
let task = Task {
try await pipeline.image(for: Test.url)
}
var caughtError: Error?
do {
_ = try await task.value
} catch {
caughtError = error
}
XCTAssertTrue(caughtError is CancellationError)
}
func testCancelImmediately() async throws {
dataLoader.queue.isSuspended = true
@ -174,24 +152,25 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable {
XCTAssertEqual(recordedProgress, [])
}
func testCancelAsyncImageTask() async throws {
dataLoader.queue.isSuspended = true
pipeline.queue.suspend()
let task = pipeline.imageTask(with: Test.url)
observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in
task.cancel()
}
pipeline.queue.resume()
var caughtError: Error?
do {
_ = try await task.image
} catch {
caughtError = error
}
XCTAssertTrue(caughtError is CancellationError)
}
#warning("reimplement")
// func testCancelAsyncImageTask() async throws {
// dataLoader.queue.isSuspended = true
//
// pipeline.queue.suspend()
// let task = pipeline.imageTask(with: Test.url)
// observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in
// task.cancel()
// }
// pipeline.queue.resume()
//
// var caughtError: Error?
// do {
// _ = try await task.image
// } catch {
// caughtError = error
// }
// XCTAssertTrue(caughtError is CancellationError)
// }
// MARK: - Load Data
@ -224,17 +203,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable {
XCTAssertTrue(caughtError is CancellationError)
}
func testImageTaskReturnedImmediately() async throws {
// GIVEN
pipelineDelegate.onTaskCreated = { [unowned self] in imageTask = $0 }
// WHEN
_ = try await pipeline.image(for: Test.request)
// THEN
XCTAssertNotNil(imageTask)
}
func testProgressUpdated() async throws {
// GIVEN
dataLoader.results[Test.url] = .success(
@ -438,39 +406,3 @@ private struct URLError: Swift.Error {
case constrained
}
}
#if swift(>=6.0)
extension ImageTask.Event: @retroactive Equatable {
public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool {
switch (lhs, rhs) {
case let (.progress(lhs), .progress(rhs)):
return lhs == rhs
case let (.preview(lhs), .preview(rhs)):
return lhs == rhs
case (.cancelled, .cancelled):
return true
case let (.finished(lhs), .finished(rhs)):
return lhs == rhs
default:
return false
}
}
}
#else
extension ImageTask.Event: Equatable {
public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool {
switch (lhs, rhs) {
case let (.progress(lhs), .progress(rhs)):
return lhs == rhs
case let (.preview(lhs), .preview(rhs)):
return lhs == rhs
case (.cancelled, .cancelled):
return true
case let (.finished(lhs), .finished(rhs)):
return lhs == rhs
default:
return false
}
}
}
#endif

View File

@ -31,7 +31,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
// When loading images for those requests
// Then the correct proessors are applied.
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
let image = result.value?.image
XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"])
@ -58,7 +58,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
// When loading images for those requests
// Then the correct proessors are applied.
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
let image = result.value?.image
XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"])
@ -87,7 +87,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
// When loading images for those requests
// Then the correct proessors are applied.
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
let image = result.value?.image
XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"])
@ -111,7 +111,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))
let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .returnCacheDataDontLoad, timeoutInterval: 0))
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1)
expect(pipeline).toLoadImage(with: request2)
}
@ -130,7 +130,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
let request2 = ImageRequest(url: Test.url, userInfo: [.scaleKey: 3])
// WHEN loading images for those requests
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
// THEN
guard let image = result.value?.image else { return XCTFail() }
@ -156,7 +156,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
let request1 = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(maxPixelSize: 400)])
let request2 = ImageRequest(url: Test.url)
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
// WHEN loading images for those requests
expect(pipeline).toLoadImage(with: request1) { result in
@ -184,7 +184,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
let request1 = ImageRequest(url: Test.url)
let request2 = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(maxPixelSize: 400)])
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
// WHEN loading images for those requests
expect(pipeline).toLoadImage(with: request1) { result in
// THEN
@ -214,7 +214,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
let queueObserver = OperationQueueObserver(queue: pipeline.configuration.imageProcessingQueue)
// When
suspendDataLoading(for: pipeline, expectedRequestCount: 3) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 3) {
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "1")]))
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "2")]))
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "1")]))
@ -428,7 +428,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
// MARK: - Loading Data
func testThatLoadsDataOnceWhenLoadingDataAndLoadingImage() {
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: Test.request)
expect(pipeline).toLoadData(with: Test.request)
}
@ -446,7 +446,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
)
// When/Then
suspendDataLoading(for: pipeline, expectedRequestCount: 3) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 3) {
for _ in 0..<3 {
let request = Test.request
@ -475,7 +475,7 @@ class ImagePipelineCoalescingTests: XCTestCase {
}
// When/Then
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: Test.request)
expect(pipeline).toLoadImage(with: Test.request)
}
@ -507,7 +507,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase {
let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2")])
// When
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
let image = result.value?.image
XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"])
@ -536,7 +536,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase {
let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2"), processors.make(id: "3")])
// When
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1)
expect(pipeline).toLoadImage(with: request2)
}
@ -613,7 +613,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase {
let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2")])
// When
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: request1) { result in
let image = result.value?.image
XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"])
@ -642,7 +642,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase {
func makeRequest(options: ImageRequest.Options) -> ImageRequest {
ImageRequest(urlRequest: URLRequest(url: Test.url), options: options)
}
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: makeRequest(options: []))
expect(pipeline).toLoadImage(with: makeRequest(options: [.reloadIgnoringCachedData]))
}
@ -666,7 +666,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase {
ImageRequest(urlRequest: URLRequest(url: Test.url), options: options)
}
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: makeRequest(options: []))
expect(pipeline).toLoadImage(with: makeRequest(options: [.reloadIgnoringCachedData]))
}

View File

@ -1,89 +0,0 @@
// The MIT License (MIT)
//
// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean).
import XCTest
@testable import Nuke
class ImagePipelineConfigurationTests: XCTestCase {
func testImageIsLoadedWithRateLimiterDisabled() {
// Given
let dataLoader = MockDataLoader()
let pipeline = ImagePipeline {
$0.dataLoader = dataLoader
$0.imageCache = nil
$0.isRateLimiterEnabled = false
}
// When/Then
expect(pipeline).toLoadImage(with: Test.request)
wait()
}
// MARK: DataCache
func testWithDataCache() {
let pipeline = ImagePipeline(configuration: .withDataCache)
XCTAssertNotNil(pipeline.configuration.dataCache)
}
// MARK: Changing Callback Queue
func testChangingCallbackQueueLoadImage() {
// Given
let queue = DispatchQueue(label: "testChangingCallbackQueue")
let queueKey = DispatchSpecificKey<Void>()
queue.setSpecific(key: queueKey, value: ())
let dataLoader = MockDataLoader()
let pipeline = ImagePipeline {
$0.dataLoader = dataLoader
$0.imageCache = nil
$0._callbackQueue = queue
}
// When/Then
let expectation = self.expectation(description: "Image Loaded")
pipeline.loadImage(with: Test.request, progress: { _, _, _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
}, completion: { _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
expectation.fulfill()
})
wait()
}
func testChangingCallbackQueueLoadData() {
// Given
let queue = DispatchQueue(label: "testChangingCallbackQueue")
let queueKey = DispatchSpecificKey<Void>()
queue.setSpecific(key: queueKey, value: ())
let dataLoader = MockDataLoader()
let pipeline = ImagePipeline {
$0.dataLoader = dataLoader
$0.imageCache = nil
$0._callbackQueue = queue
}
// When/Then
let expectation = self.expectation(description: "Image data Loaded")
pipeline.loadData(with: Test.request, progress: { _, _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
}, completion: { _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
expectation.fulfill()
})
wait()
}
func testEnablingSignposts() {
ImagePipeline.Configuration.isSignpostLoggingEnabled = false // Just padding
ImagePipeline.Configuration.isSignpostLoggingEnabled = true
ImagePipeline.Configuration.isSignpostLoggingEnabled = false
}
}

View File

@ -393,7 +393,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url))
}
@ -480,7 +480,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url))
}
@ -545,7 +545,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url))
}

View File

@ -89,7 +89,7 @@ class ImagePipelineDelegateTests: XCTestCase {
}
}
private final class MockImagePipelineDelegate: ImagePipelineDelegate, @unchecked Sendable {
private final class MockImagePipelineDelegate: ImagePipeline.Delegate, @unchecked Sendable {
var isCacheEnabled = true
func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? {

View File

@ -31,7 +31,9 @@ class ImagePipelineLoadDataTests: XCTestCase {
func testLoadDataDataLoaded() {
let expectation = self.expectation(description: "Image data Loaded")
pipeline.loadData(with: Test.request) { result in
let response = try! XCTUnwrap(result.value)
guard let response = result.value else {
return XCTFail()
}
XCTAssertEqual(response.data.count, 22789)
XCTAssertTrue(Thread.isMainThread)
expectation.fulfill()
@ -89,25 +91,6 @@ class ImagePipelineLoadDataTests: XCTestCase {
wait()
}
// MARK: - Callback Queues
func testChangingCallbackQueueLoadData() {
// GIVEN
let queue = DispatchQueue(label: "testChangingCallbackQueue")
let queueKey = DispatchSpecificKey<Void>()
queue.setSpecific(key: queueKey, value: ())
// WHEN/THEN
let expectation = self.expectation(description: "Image data Loaded")
pipeline.loadData(with: Test.request, queue: queue, progress: { _, _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
}, completion: { _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
expectation.fulfill()
})
wait()
}
// MARK: - Errors
func testLoadWithInvalidURL() throws {
@ -213,7 +196,7 @@ extension ImagePipelineLoadDataTests {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url))
}
@ -277,7 +260,7 @@ extension ImagePipelineLoadDataTests {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url))
}
@ -403,7 +386,7 @@ extension ImagePipelineLoadDataTests {
}
// WHEN
suspendDataLoading(for: pipeline, expectedRequestCount: 2) {
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")]))
expect(pipeline).toLoadData(with: ImageRequest(url: Test.url))
}
@ -420,7 +403,7 @@ extension ImagePipelineLoadDataTests {
}
extension XCTestCase {
func suspendDataLoading(for pipeline: ImagePipeline, expectedRequestCount count: Int, _ closure: () -> Void) {
func withSuspendedDataLoader(for pipeline: ImagePipeline, expectedRequestCount count: Int, _ closure: () -> Void) {
let dataLoader = pipeline.configuration.dataLoader as! MockDataLoader
dataLoader.isSuspended = true
let expectation = self.expectation(description: "registered")

View File

@ -48,17 +48,36 @@ class ImagePipelineResumableDataTests: XCTestCase {
}
}
private class _MockResumableDataLoader: DataLoading, @unchecked Sendable {
private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked Sendable {
private let queue = DispatchQueue(label: "_MockResumableDataLoader")
let data: Data = Test.data(name: "fixture", extension: "jpeg")
let eTag: String = "img_01"
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable {
func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> {
AsyncThrowingStream { continuation in
guard let urlRequest = request.urlRequest else {
return continuation.finish(throwing: URLError(.badURL))
}
let task = loadData(with: urlRequest) { data, response in
continuation.yield((data, response))
} completion: { error in
continuation.finish(throwing: error)
}
continuation.onTermination = { reason in
switch reason {
case .cancelled: task.cancel()
default: break
}
}
}
}
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol {
let headers = request.allHTTPHeaderFields
let completion = UncheckedSendableBox(value: completion)
let didReceiveData = UncheckedSendableBox(value: didReceiveData)
let completion = completion
let didReceiveData = didReceiveData
func sendChunks(_ chunks: [Data], of data: Data, statusCode: Int) {
@Sendable func sendChunk(_ chunk: Data) {
@ -74,7 +93,7 @@ private class _MockResumableDataLoader: DataLoading, @unchecked Sendable {
]
)!
didReceiveData.value(chunk, response)
didReceiveData(chunk, response)
}
var chunks = chunks
@ -102,7 +121,7 @@ private class _MockResumableDataLoader: DataLoading, @unchecked Sendable {
sendChunks(chunks, of: remainingData, statusCode: 206)
queue.async {
completion.value(nil)
completion(nil)
}
} else {
// Send half of chunks.
@ -111,14 +130,14 @@ private class _MockResumableDataLoader: DataLoading, @unchecked Sendable {
sendChunks(chunks, of: data, statusCode: 200)
queue.async {
completion.value(NSError(domain: NSURLErrorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: [:]))
completion(NSError(domain: NSURLErrorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: [:]))
}
}
return _Task()
}
private class _Task: Cancellable, @unchecked Sendable {
private class _Task: MockDataTaskProtocol, @unchecked Sendable {
func cancel() { }
}
}

View File

@ -29,10 +29,8 @@ class ImagePipelineTaskDelegateTests: XCTestCase {
// Then
XCTAssertEqual(delegate.events, [
ImageTaskEvent.created,
.started,
.progressUpdated(completedUnitCount: 22789, totalUnitCount: 22789),
.completed(result: try XCTUnwrap(result))
.progress(.init(completed: 22789, total: 22789)),
.finished(try XCTUnwrap(result))
])
}
@ -48,11 +46,9 @@ class ImagePipelineTaskDelegateTests: XCTestCase {
// Then
XCTAssertEqual(delegate.events, [
ImageTaskEvent.created,
.started,
.progressUpdated(completedUnitCount: 10, totalUnitCount: 20),
.progressUpdated(completedUnitCount: 20, totalUnitCount: 20),
.completed(result: try XCTUnwrap(result))
.progress(.init(completed: 10, total: 20)),
.progress(.init(completed: 20, total: 20)),
.finished(try XCTUnwrap(result))
])
}
@ -71,8 +67,6 @@ class ImagePipelineTaskDelegateTests: XCTestCase {
// Then
XCTAssertEqual(delegate.events, [
ImageTaskEvent.created,
.started,
.cancelled
])
}

View File

@ -81,26 +81,7 @@ class ImagePipelineTests: XCTestCase {
wait()
}
// MARK: - Callback Queues
func testChangingCallbackQueueLoadImage() {
// Given
let queue = DispatchQueue(label: "testChangingCallbackQueue")
let queueKey = DispatchSpecificKey<Void>()
queue.setSpecific(key: queueKey, value: ())
// When/Then
let expectation = self.expectation(description: "Image Loaded")
pipeline.loadImage(with: Test.request, queue: queue, progress: { _, _, _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
}, completion: { _ in
XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey))
expectation.fulfill()
})
wait()
}
// MARK: - Updating Priority
func testDataLoadingPriorityUpdated() {
@ -569,20 +550,6 @@ class ImagePipelineTests: XCTestCase {
wait()
}
func testSkipDataLoadingQueuePerRequestWithPublisher() throws {
// Given
let queue = pipeline.configuration.dataLoadingQueue
queue.isSuspended = true
let request = ImageRequest(id: "a", dataPublisher: Just(Test.data), options: [
.skipDataLoadingQueue
])
// Then image is still loaded
expect(pipeline).toLoadImage(with: request)
wait()
}
// MARK: Misc
func testLoadWithStringLiteral() async throws {

View File

@ -38,21 +38,16 @@ final class ImagePrefetcherTests: XCTestCase {
/// Start prefetching for the request and then request an image separarely.
func testBasicScenario() {
dataLoader.isSuspended = true
expect(prefetcher.queue).toEnqueueOperationsWithCount(1)
prefetcher.startPrefetching(with: [Test.request])
wait()
expect(pipeline).toLoadImage(with: Test.request)
pipeline.queue.async { [dataLoader] in
dataLoader?.isSuspended = false
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) {
expect(prefetcher.queue).toEnqueueOperationsWithCount(1)
prefetcher.startPrefetching(with: [Test.request])
expect(pipeline).toLoadImage(with: Test.request)
}
wait()
// THEN
XCTAssertEqual(dataLoader.createdTaskCount, 1)
XCTAssertEqual(observer.startedTaskCount, 2)
XCTAssertEqual(observer.createdTaskCount, 2)
}
// MARK: Start Prefetching
@ -71,34 +66,17 @@ final class ImagePrefetcherTests: XCTestCase {
}
func testStartPrefetchingWithTwoEquivalentURLs() {
dataLoader.isSuspended = true
expectPrefetcherToComplete()
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 1) {
expectPrefetcherToComplete()
// WHEN
prefetcher.startPrefetching(with: [Test.url])
prefetcher.startPrefetching(with: [Test.url])
pipeline.queue.async { [dataLoader] in
dataLoader?.isSuspended = false
// WHEN
prefetcher.startPrefetching(with: [Test.url])
prefetcher.startPrefetching(with: [Test.url])
}
wait()
// THEN only one task is started
XCTAssertEqual(observer.startedTaskCount, 1)
}
func testWhenImageIsInMemoryCacheNoTaskStarted() {
dataLoader.isSuspended = true
// GIVEN
pipeline.cache[Test.request] = Test.container
// WHEN
prefetcher.startPrefetching(with: [Test.url])
pipeline.queue.sync {}
// THEN
XCTAssertEqual(observer.startedTaskCount, 0)
XCTAssertEqual(observer.createdTaskCount, 1)
}
// MARK: Stop Prefetching
@ -108,7 +86,7 @@ final class ImagePrefetcherTests: XCTestCase {
// WHEN
let url = Test.url
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
expectNotification(ImagePipelineObserver.didCreateTask, object: observer)
prefetcher.startPrefetching(with: [url])
wait()
@ -149,13 +127,13 @@ final class ImagePrefetcherTests: XCTestCase {
prefetcher.startPrefetching(with: [Test.url])
let expectation = self.expectation(description: "TimePassed")
pipeline.queue.asyncAfter(deadline: .now() + .milliseconds(10)) {
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(20)) {
expectation.fulfill()
}
wait()
// THEN
XCTAssertEqual(observer.startedTaskCount, 0)
XCTAssertEqual(observer.createdTaskCount, 0)
}
// MARK: Priority
@ -246,7 +224,7 @@ final class ImagePrefetcherTests: XCTestCase {
func testDidCompleteIsCalled() {
let expectation = self.expectation(description: "PrefecherDidComplete")
prefetcher.didComplete = { @MainActor @Sendable in
prefetcher.didComplete.value = { @Sendable in
expectation.fulfill()
}
@ -256,7 +234,7 @@ final class ImagePrefetcherTests: XCTestCase {
func testDidCompleteIsCalledWhenImageCached() {
let expectation = self.expectation(description: "PrefecherDidComplete")
prefetcher.didComplete = { @MainActor @Sendable in
prefetcher.didComplete.value = { @Sendable in
expectation.fulfill()
}
@ -272,7 +250,7 @@ final class ImagePrefetcherTests: XCTestCase {
pipeline.configuration.dataLoadingQueue.isSuspended = true
let request = Test.request
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
expectNotification(ImagePipelineObserver.didCreateTask, object: observer)
prefetcher.startPrefetching(with: [request])
wait()
@ -285,7 +263,7 @@ final class ImagePrefetcherTests: XCTestCase {
func expectPrefetcherToComplete() {
let expectation = self.expectation(description: "PrefecherDidComplete")
prefetcher.didComplete = { @MainActor @Sendable in
prefetcher.didComplete.value = { @Sendable in
expectation.fulfill()
}
}

View File

@ -6,33 +6,25 @@ import XCTest
@testable import Nuke
class RateLimiterTests: XCTestCase {
var queue: DispatchQueue!
var queueKey: DispatchSpecificKey<Void>!
var rateLimiter: RateLimiter!
override func setUp() {
super.setUp()
queue = DispatchQueue(label: "com.github.kean.rate-limiter-tests")
queueKey = DispatchSpecificKey<Void>()
queue.setSpecific(key: queueKey, value: ())
// Note: we set very short rate to avoid bucket form being refilled too quickly
rateLimiter = RateLimiter(queue: queue, rate: 10, burst: 2)
rateLimiter = RateLimiter(rate: 10, burst: 2)
}
@ImagePipelineActor
func testThatBurstIsExecutedimmediately() {
// Given
var isExecuted = Array(repeating: false, count: 4)
// When
for i in isExecuted.indices {
queue.sync {
rateLimiter.execute {
isExecuted[i] = true
return true
}
rateLimiter.execute {
isExecuted[i] = true
return true
}
}
@ -40,68 +32,44 @@ class RateLimiterTests: XCTestCase {
XCTAssertEqual(isExecuted, [true, true, false, false], "Expect first 2 items to be executed immediately")
}
@ImagePipelineActor
func testThatNotExecutedItemDoesntExtractFromBucket() {
// Given
var isExecuted = Array(repeating: false, count: 4)
// When
for i in isExecuted.indices {
queue.sync {
rateLimiter.execute {
isExecuted[i] = true
return i != 1 // important!
}
rateLimiter.execute {
isExecuted[i] = true
return i != 1 // important!
}
}
// Then
XCTAssertEqual(isExecuted, [true, true, true, false], "Expect first 2 items to be executed immediately")
}
@ImagePipelineActor
func testOverflow() {
// Given
var isExecuted = Array(repeating: false, count: 3)
// When
let expectation = self.expectation(description: "All work executed")
expectation.expectedFulfillmentCount = isExecuted.count
queue.sync {
for i in isExecuted.indices {
rateLimiter.execute {
isExecuted[i] = true
expectation.fulfill()
return true
}
for i in isExecuted.indices {
rateLimiter.execute {
isExecuted[i] = true
expectation.fulfill()
return true
}
}
// When time is passed
wait()
// Then
queue.sync {
XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay")
}
}
func testOverflowItemsExecutedOnSpecificQueue() {
// Given
let isExecuted = Array(repeating: false, count: 3)
let expectation = self.expectation(description: "All work executed")
expectation.expectedFulfillmentCount = isExecuted.count
queue.sync {
for _ in isExecuted.indices {
rateLimiter.execute {
expectation.fulfill()
// Then delayed task also executed on queue
XCTAssertNotNil(DispatchQueue.getSpecific(key: self.queueKey))
return true
}
}
}
wait()
XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay")
}
}

View File

@ -5,6 +5,7 @@
import XCTest
@testable import Nuke
@ImagePipelineActor
class TaskTests: XCTestCase {
// MARK: - Starter

View File

@ -106,66 +106,4 @@ class FetchImageTests: XCTestCase {
image.priority = .high
wait()
}
func testPublisherImageLoaded() throws {
// RECORD
let record = expect(image.$result.dropFirst()).toPublishSingleValue()
// WHEN
image.load(pipeline.imagePublisher(with: Test.request))
wait()
// THEN
let result = try XCTUnwrap(try XCTUnwrap(record.last))
XCTAssertTrue(result.isSuccess)
XCTAssertNotNil(image.image)
}
func testPublisherIsLoadingUpdated() {
// RECORD
expect(image.$result.dropFirst()).toPublishSingleValue()
let isLoading = record(image.$isLoading)
// WHEN
image.load(pipeline.imagePublisher(with: Test.request))
wait()
// THEN
XCTAssertEqual(isLoading.values, [false, true, false])
}
func testPublisherMemoryCacheLookup() throws {
// GIVEN
pipeline.cache[Test.request] = Test.container
// WHEN
image.load(pipeline.imagePublisher(with: Test.request))
// THEN image loaded synchronously
let result = try XCTUnwrap(image.result)
XCTAssertTrue(result.isSuccess)
let response = try XCTUnwrap(result.value)
XCTAssertEqual(response.cacheType, .memory)
XCTAssertNotNil(image.image)
}
func testRequestCancelledWhenTargetGetsDeallocated() {
dataLoader.isSuspended = true
// Wrap everything in autorelease pool to make sure that imageView
// gets deallocated immediately.
autoreleasepool {
// Given an image view with an associated image task
expectNotification(ImagePipelineObserver.didStartTask, object: observer)
image.load(pipeline.imagePublisher(with: Test.request))
wait()
// Expect the task to be cancelled automatically
expectNotification(ImagePipelineObserver.didCancelTask, object: observer)
// When the fetch image instance is deallocated
image = nil
}
wait()
}
}