mirror of
https://github.com/kean/Nuke.git
synced 2024-11-28 12:04:01 +03:00
Merge 1c495bd9d8
into 1d748d4eac
This commit is contained in:
commit
69951b0485
@ -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
|
||||
|
@ -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 */,
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
}
|
||||
|
107
Sources/Nuke/Pipeline/ImagePipeline+Closures.swift
Normal file
107
Sources/Nuke/Pipeline/ImagePipeline+Closures.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
13
Sources/Nuke/Pipeline/ImagePipelineActor.swift
Normal file
13
Sources/Nuke/Pipeline/ImagePipelineActor.swift
Normal 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
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
///
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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")!)
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -26,7 +26,8 @@ class ImageViewIntegrationTests: XCTestCase {
|
||||
|
||||
imageView = _ImageView()
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
|
@ -28,7 +28,8 @@ class ImageViewLoadingOptionsTests: XCTestCase {
|
||||
|
||||
imageView = _ImageView()
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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]))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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? {
|
||||
|
@ -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")
|
||||
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
])
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
import XCTest
|
||||
@testable import Nuke
|
||||
|
||||
@ImagePipelineActor
|
||||
class TaskTests: XCTestCase {
|
||||
// MARK: - Starter
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user