├── .gitignore ├── NukeDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── NukeDemo.xcscheme ├── README.md ├── Resources ├── IB │ └── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 50.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ ├── Contents.json │ └── nuke-logo.imageset │ │ ├── 114792417-57c1d080-9d56-11eb-8035-dc07cfd7557f.png │ │ └── Contents.json ├── Info.plist ├── baseline.jpeg └── progressive.jpeg └── Sources ├── AlamofireIntegrationDemoViewController.swift ├── AnimatedImageUsingVideoViewController.swift ├── AppDelegate.swift ├── BasicDemoViewController.swift ├── DataCachingDemoViewController.swift ├── GifuDemoViewController.swift ├── Helpers ├── GIFImage.swift ├── Images.swift └── Utilities.swift ├── ImagePipelineSettingsViewController.storyboard ├── ImagePipelineSettingsViewController.swift ├── ImageProcessingDemoViewController.swift ├── LazyImageDemo.swift ├── MenuViewController.swift ├── PrefetchSwiftUIDemo.swift ├── PrefetchingDemoViewController.swift ├── ProgressiveDecodingDemoViewController.swift ├── RateLimiterDemoViewController.swift └── SwiftSVGDemoViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ## System 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | Nuke.xcodeproj/xcshareddata/xcbaselines/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | 35 | ## Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | 40 | .build/ 41 | 42 | 43 | ## CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | 50 | Pods/ 51 | 52 | 53 | ## Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | 57 | Carthage 58 | -------------------------------------------------------------------------------- /NukeDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0C063F9226651F000018F2C2 /* LazyImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C063F9126651F000018F2C2 /* LazyImageDemo.swift */; }; 11 | 0C1B4BAE2618D0100095E7F7 /* PrefetchSwiftUIDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1B4BAD2618D0100095E7F7 /* PrefetchSwiftUIDemo.swift */; }; 12 | 0C3ADAA32087D23C00FFA275 /* progressive.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C3ADAA12087D23C00FFA275 /* progressive.jpeg */; }; 13 | 0C3ADAA42087D23C00FFA275 /* baseline.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C3ADAA22087D23C00FFA275 /* baseline.jpeg */; }; 14 | 0C4388A822AC060F007906A7 /* ImageProcessingDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4388A722AC060F007906A7 /* ImageProcessingDemoViewController.swift */; }; 15 | 0C4878AA22A84E9700CFCFE7 /* ImagePipelineSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4878A922A84E9700CFCFE7 /* ImagePipelineSettingsViewController.swift */; }; 16 | 0C4878AC22A84EF900CFCFE7 /* ImagePipelineSettingsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C4878AB22A84EF900CFCFE7 /* ImagePipelineSettingsViewController.storyboard */; }; 17 | 0C880535242EB72E00F8C5B3 /* SwiftSVG in Frameworks */ = {isa = PBXBuildFile; productRef = 0C880534242EB72E00F8C5B3 /* SwiftSVG */; }; 18 | 0C880537242EB74B00F8C5B3 /* SwiftSVGDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C880536242EB74B00F8C5B3 /* SwiftSVGDemoViewController.swift */; }; 19 | 0C895239288A07A3003BC1C2 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 0C895238288A07A3003BC1C2 /* Alamofire */; }; 20 | 0C9E5FBA28AC4AF600FC1320 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 0C9E5FB928AC4AF600FC1320 /* Nuke */; }; 21 | 0C9E5FBC28AC4AF600FC1320 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 0C9E5FBB28AC4AF600FC1320 /* NukeExtensions */; }; 22 | 0C9E5FBE28AC4AF600FC1320 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0C9E5FBD28AC4AF600FC1320 /* NukeUI */; }; 23 | 0C9E5FC028AC4B0200FC1320 /* Pulse in Frameworks */ = {isa = PBXBuildFile; productRef = 0C9E5FBF28AC4B0200FC1320 /* Pulse */; }; 24 | 0CBBB09B20A8116C007474D2 /* AnimatedImageUsingVideoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBBB09A20A8116C007474D2 /* AnimatedImageUsingVideoViewController.swift */; }; 25 | 0CC841A329CF3CCA0042A3A4 /* Gifu in Frameworks */ = {isa = PBXBuildFile; productRef = 0CC841A229CF3CCA0042A3A4 /* Gifu */; }; 26 | 0CC841A529CF4ADF0042A3A4 /* NukeVideo in Frameworks */ = {isa = PBXBuildFile; productRef = 0CC841A429CF4ADF0042A3A4 /* NukeVideo */; }; 27 | 0CC841A729CF532E0042A3A4 /* GIFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC841A629CF532E0042A3A4 /* GIFImage.swift */; }; 28 | 0CCC1883209EDAF9003D7172 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CCC1882209EDAF9003D7172 /* Images.xcassets */; }; 29 | 0CCC1894209EDCCF003D7172 /* BasicDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC1887209EDCCF003D7172 /* BasicDemoViewController.swift */; }; 30 | 0CCC1896209EDCCF003D7172 /* RateLimiterDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC1889209EDCCF003D7172 /* RateLimiterDemoViewController.swift */; }; 31 | 0CCC1898209EDCCF003D7172 /* DataCachingDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC188B209EDCCF003D7172 /* DataCachingDemoViewController.swift */; }; 32 | 0CCC1899209EDCCF003D7172 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC188D209EDCCF003D7172 /* AppDelegate.swift */; }; 33 | 0CCC189A209EDCCF003D7172 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC188F209EDCCF003D7172 /* Utilities.swift */; }; 34 | 0CCC189C209EDCCF003D7172 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC1892209EDCCF003D7172 /* MenuViewController.swift */; }; 35 | 0CCC189D209EDCCF003D7172 /* ProgressiveDecodingDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC1893209EDCCF003D7172 /* ProgressiveDecodingDemoViewController.swift */; }; 36 | 0CCC18A3209EE0A6003D7172 /* GifuDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC18A2209EE0A6003D7172 /* GifuDemoViewController.swift */; }; 37 | 0CCC18A8209EE341003D7172 /* AlamofireIntegrationDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC18A7209EE341003D7172 /* AlamofireIntegrationDemoViewController.swift */; }; 38 | 0CCF787F28A03BDC00C431E5 /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = 0CCF787E28A03BDC00C431E5 /* PulseUI */; }; 39 | 0CD878972431108200DE301A /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD878962431108200DE301A /* Images.swift */; }; 40 | 0CE5F687215688590046609F /* PrefetchingDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE5F686215688590046609F /* PrefetchingDemoViewController.swift */; }; 41 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 42 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 43 | /* End PBXBuildFile section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 0C063F9126651F000018F2C2 /* LazyImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImageDemo.swift; sourceTree = ""; }; 47 | 0C1B4BAD2618D0100095E7F7 /* PrefetchSwiftUIDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchSwiftUIDemo.swift; sourceTree = ""; }; 48 | 0C3ADAA12087D23C00FFA275 /* progressive.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = progressive.jpeg; sourceTree = ""; }; 49 | 0C3ADAA22087D23C00FFA275 /* baseline.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = baseline.jpeg; sourceTree = ""; }; 50 | 0C4388A722AC060F007906A7 /* ImageProcessingDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessingDemoViewController.swift; sourceTree = ""; }; 51 | 0C4878A922A84E9700CFCFE7 /* ImagePipelineSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineSettingsViewController.swift; sourceTree = ""; }; 52 | 0C4878AB22A84EF900CFCFE7 /* ImagePipelineSettingsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ImagePipelineSettingsViewController.storyboard; sourceTree = ""; }; 53 | 0C880536242EB74B00F8C5B3 /* SwiftSVGDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftSVGDemoViewController.swift; sourceTree = ""; }; 54 | 0CB1D1D62372FD4B0041D8D6 /* DFCache.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DFCache.framework; path = Carthage/Build/iOS/DFCache.framework; sourceTree = ""; }; 55 | 0CB1D1DA2372FD580041D8D6 /* FLAnimatedImage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FLAnimatedImage.framework; path = Carthage/Build/iOS/FLAnimatedImage.framework; sourceTree = ""; }; 56 | 0CBBB09A20A8116C007474D2 /* AnimatedImageUsingVideoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImageUsingVideoViewController.swift; sourceTree = ""; }; 57 | 0CC841A629CF532E0042A3A4 /* GIFImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImage.swift; sourceTree = ""; }; 58 | 0CCC1882209EDAF9003D7172 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 59 | 0CCC1884209EDC01003D7172 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 0CCC1887209EDCCF003D7172 /* BasicDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicDemoViewController.swift; sourceTree = ""; }; 61 | 0CCC1889209EDCCF003D7172 /* RateLimiterDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimiterDemoViewController.swift; sourceTree = ""; }; 62 | 0CCC188B209EDCCF003D7172 /* DataCachingDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataCachingDemoViewController.swift; sourceTree = ""; }; 63 | 0CCC188D209EDCCF003D7172 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 64 | 0CCC188F209EDCCF003D7172 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 65 | 0CCC1892209EDCCF003D7172 /* MenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; }; 66 | 0CCC1893209EDCCF003D7172 /* ProgressiveDecodingDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressiveDecodingDemoViewController.swift; sourceTree = ""; }; 67 | 0CCC18A2209EE0A6003D7172 /* GifuDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifuDemoViewController.swift; sourceTree = ""; }; 68 | 0CCC18A7209EE341003D7172 /* AlamofireIntegrationDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireIntegrationDemoViewController.swift; sourceTree = ""; }; 69 | 0CD878962431108200DE301A /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; 70 | 0CE5F686215688590046609F /* PrefetchingDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefetchingDemoViewController.swift; sourceTree = ""; }; 71 | 607FACD01AFB9204008FA782 /* NukeDemo_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NukeDemo_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 73 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 74 | /* End PBXFileReference section */ 75 | 76 | /* Begin PBXFrameworksBuildPhase section */ 77 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | 0C880535242EB72E00F8C5B3 /* SwiftSVG in Frameworks */, 82 | 0C9E5FBC28AC4AF600FC1320 /* NukeExtensions in Frameworks */, 83 | 0C895239288A07A3003BC1C2 /* Alamofire in Frameworks */, 84 | 0C9E5FBA28AC4AF600FC1320 /* Nuke in Frameworks */, 85 | 0CCF787F28A03BDC00C431E5 /* PulseUI in Frameworks */, 86 | 0C9E5FC028AC4B0200FC1320 /* Pulse in Frameworks */, 87 | 0C9E5FBE28AC4AF600FC1320 /* NukeUI in Frameworks */, 88 | 0CC841A529CF4ADF0042A3A4 /* NukeVideo in Frameworks */, 89 | 0CC841A329CF3CCA0042A3A4 /* Gifu in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | 0C3ADAA02087D23C00FFA275 /* Resources */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 0CCC1885209EDC40003D7172 /* IB */, 100 | 0CCC1884209EDC01003D7172 /* Info.plist */, 101 | 0CCC1882209EDAF9003D7172 /* Images.xcassets */, 102 | 0C3ADAA12087D23C00FFA275 /* progressive.jpeg */, 103 | 0C3ADAA22087D23C00FFA275 /* baseline.jpeg */, 104 | ); 105 | path = Resources; 106 | sourceTree = SOURCE_ROOT; 107 | }; 108 | 0CCC1885209EDC40003D7172 /* IB */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 112 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 113 | ); 114 | path = IB; 115 | sourceTree = ""; 116 | }; 117 | 0CCC1886209EDCCF003D7172 /* Sources */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 0CCC188D209EDCCF003D7172 /* AppDelegate.swift */, 121 | 0CCC1892209EDCCF003D7172 /* MenuViewController.swift */, 122 | 0CCC1887209EDCCF003D7172 /* BasicDemoViewController.swift */, 123 | 0C4388A722AC060F007906A7 /* ImageProcessingDemoViewController.swift */, 124 | 0CCC18A2209EE0A6003D7172 /* GifuDemoViewController.swift */, 125 | 0CBBB09A20A8116C007474D2 /* AnimatedImageUsingVideoViewController.swift */, 126 | 0CCC18A7209EE341003D7172 /* AlamofireIntegrationDemoViewController.swift */, 127 | 0CE5F686215688590046609F /* PrefetchingDemoViewController.swift */, 128 | 0C1B4BAD2618D0100095E7F7 /* PrefetchSwiftUIDemo.swift */, 129 | 0C063F9126651F000018F2C2 /* LazyImageDemo.swift */, 130 | 0CCC1889209EDCCF003D7172 /* RateLimiterDemoViewController.swift */, 131 | 0CCC188B209EDCCF003D7172 /* DataCachingDemoViewController.swift */, 132 | 0CCC1893209EDCCF003D7172 /* ProgressiveDecodingDemoViewController.swift */, 133 | 0C4878A922A84E9700CFCFE7 /* ImagePipelineSettingsViewController.swift */, 134 | 0C4878AB22A84EF900CFCFE7 /* ImagePipelineSettingsViewController.storyboard */, 135 | 0C880536242EB74B00F8C5B3 /* SwiftSVGDemoViewController.swift */, 136 | 0CCC188E209EDCCF003D7172 /* Helpers */, 137 | ); 138 | path = Sources; 139 | sourceTree = ""; 140 | }; 141 | 0CCC188E209EDCCF003D7172 /* Helpers */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 0CCC188F209EDCCF003D7172 /* Utilities.swift */, 145 | 0CD878962431108200DE301A /* Images.swift */, 146 | 0CC841A629CF532E0042A3A4 /* GIFImage.swift */, 147 | ); 148 | path = Helpers; 149 | sourceTree = ""; 150 | }; 151 | 607FACC71AFB9204008FA782 = { 152 | isa = PBXGroup; 153 | children = ( 154 | 0CCC1886209EDCCF003D7172 /* Sources */, 155 | 0C3ADAA02087D23C00FFA275 /* Resources */, 156 | 607FACD11AFB9204008FA782 /* Products */, 157 | B268730C38EF50AF5D1A33EC /* Frameworks */, 158 | ); 159 | sourceTree = ""; 160 | }; 161 | 607FACD11AFB9204008FA782 /* Products */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 607FACD01AFB9204008FA782 /* NukeDemo_Example.app */, 165 | ); 166 | name = Products; 167 | sourceTree = ""; 168 | }; 169 | B268730C38EF50AF5D1A33EC /* Frameworks */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 0CB1D1DA2372FD580041D8D6 /* FLAnimatedImage.framework */, 173 | 0CB1D1D62372FD4B0041D8D6 /* DFCache.framework */, 174 | ); 175 | name = Frameworks; 176 | sourceTree = ""; 177 | }; 178 | /* End PBXGroup section */ 179 | 180 | /* Begin PBXNativeTarget section */ 181 | 607FACCF1AFB9204008FA782 /* NukeDemo */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "NukeDemo" */; 184 | buildPhases = ( 185 | 607FACCC1AFB9204008FA782 /* Sources */, 186 | 607FACCD1AFB9204008FA782 /* Frameworks */, 187 | 607FACCE1AFB9204008FA782 /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | ); 193 | name = NukeDemo; 194 | packageProductDependencies = ( 195 | 0C880534242EB72E00F8C5B3 /* SwiftSVG */, 196 | 0C895238288A07A3003BC1C2 /* Alamofire */, 197 | 0CCF787E28A03BDC00C431E5 /* PulseUI */, 198 | 0C9E5FB928AC4AF600FC1320 /* Nuke */, 199 | 0C9E5FBB28AC4AF600FC1320 /* NukeExtensions */, 200 | 0C9E5FBD28AC4AF600FC1320 /* NukeUI */, 201 | 0C9E5FBF28AC4B0200FC1320 /* Pulse */, 202 | 0CC841A229CF3CCA0042A3A4 /* Gifu */, 203 | 0CC841A429CF4ADF0042A3A4 /* NukeVideo */, 204 | ); 205 | productName = Nuke; 206 | productReference = 607FACD01AFB9204008FA782 /* NukeDemo_Example.app */; 207 | productType = "com.apple.product-type.application"; 208 | }; 209 | /* End PBXNativeTarget section */ 210 | 211 | /* Begin PBXProject section */ 212 | 607FACC81AFB9204008FA782 /* Project object */ = { 213 | isa = PBXProject; 214 | attributes = { 215 | LastSwiftUpdateCheck = 0700; 216 | LastUpgradeCheck = 1240; 217 | ORGANIZATIONNAME = CocoaPods; 218 | TargetAttributes = { 219 | 607FACCF1AFB9204008FA782 = { 220 | CreatedOnToolsVersion = 6.3.1; 221 | DevelopmentTeam = NR8DLKJ7E6; 222 | LastSwiftMigration = 1010; 223 | }; 224 | }; 225 | }; 226 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "NukeDemo" */; 227 | compatibilityVersion = "Xcode 10.0"; 228 | developmentRegion = en; 229 | hasScannedForEncodings = 0; 230 | knownRegions = ( 231 | en, 232 | Base, 233 | ); 234 | mainGroup = 607FACC71AFB9204008FA782; 235 | packageReferences = ( 236 | 0C880533242EB72E00F8C5B3 /* XCRemoteSwiftPackageReference "SwiftSVG" */, 237 | 0C895237288A07A3003BC1C2 /* XCRemoteSwiftPackageReference "Alamofire" */, 238 | 0CCF787B28A03BDC00C431E5 /* XCRemoteSwiftPackageReference "Pulse" */, 239 | 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */, 240 | 0CC841A129CF3CCA0042A3A4 /* XCRemoteSwiftPackageReference "Gifu" */, 241 | ); 242 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 243 | projectDirPath = ""; 244 | projectRoot = ""; 245 | targets = ( 246 | 607FACCF1AFB9204008FA782 /* NukeDemo */, 247 | ); 248 | }; 249 | /* End PBXProject section */ 250 | 251 | /* Begin PBXResourcesBuildPhase section */ 252 | 607FACCE1AFB9204008FA782 /* Resources */ = { 253 | isa = PBXResourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 257 | 0CCC1883209EDAF9003D7172 /* Images.xcassets in Resources */, 258 | 0C3ADAA32087D23C00FFA275 /* progressive.jpeg in Resources */, 259 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 260 | 0C3ADAA42087D23C00FFA275 /* baseline.jpeg in Resources */, 261 | 0C4878AC22A84EF900CFCFE7 /* ImagePipelineSettingsViewController.storyboard in Resources */, 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXResourcesBuildPhase section */ 266 | 267 | /* Begin PBXSourcesBuildPhase section */ 268 | 607FACCC1AFB9204008FA782 /* Sources */ = { 269 | isa = PBXSourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | 0CCC18A3209EE0A6003D7172 /* GifuDemoViewController.swift in Sources */, 273 | 0C4878AA22A84E9700CFCFE7 /* ImagePipelineSettingsViewController.swift in Sources */, 274 | 0C880537242EB74B00F8C5B3 /* SwiftSVGDemoViewController.swift in Sources */, 275 | 0CCC1899209EDCCF003D7172 /* AppDelegate.swift in Sources */, 276 | 0CCC189A209EDCCF003D7172 /* Utilities.swift in Sources */, 277 | 0C1B4BAE2618D0100095E7F7 /* PrefetchSwiftUIDemo.swift in Sources */, 278 | 0C063F9226651F000018F2C2 /* LazyImageDemo.swift in Sources */, 279 | 0CE5F687215688590046609F /* PrefetchingDemoViewController.swift in Sources */, 280 | 0CCC189C209EDCCF003D7172 /* MenuViewController.swift in Sources */, 281 | 0CD878972431108200DE301A /* Images.swift in Sources */, 282 | 0CCC1896209EDCCF003D7172 /* RateLimiterDemoViewController.swift in Sources */, 283 | 0CCC18A8209EE341003D7172 /* AlamofireIntegrationDemoViewController.swift in Sources */, 284 | 0CC841A729CF532E0042A3A4 /* GIFImage.swift in Sources */, 285 | 0CCC1898209EDCCF003D7172 /* DataCachingDemoViewController.swift in Sources */, 286 | 0CBBB09B20A8116C007474D2 /* AnimatedImageUsingVideoViewController.swift in Sources */, 287 | 0CCC189D209EDCCF003D7172 /* ProgressiveDecodingDemoViewController.swift in Sources */, 288 | 0C4388A822AC060F007906A7 /* ImageProcessingDemoViewController.swift in Sources */, 289 | 0CCC1894209EDCCF003D7172 /* BasicDemoViewController.swift in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | /* End PBXSourcesBuildPhase section */ 294 | 295 | /* Begin PBXVariantGroup section */ 296 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 297 | isa = PBXVariantGroup; 298 | children = ( 299 | 607FACDA1AFB9204008FA782 /* Base */, 300 | ); 301 | name = Main.storyboard; 302 | sourceTree = ""; 303 | }; 304 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 305 | isa = PBXVariantGroup; 306 | children = ( 307 | 607FACDF1AFB9204008FA782 /* Base */, 308 | ); 309 | name = LaunchScreen.xib; 310 | sourceTree = ""; 311 | }; 312 | /* End PBXVariantGroup section */ 313 | 314 | /* Begin XCBuildConfiguration section */ 315 | 607FACED1AFB9204008FA782 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ALWAYS_SEARCH_USER_PATHS = NO; 319 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_COMMA = YES; 327 | CLANG_WARN_CONSTANT_CONVERSION = YES; 328 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 329 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 330 | CLANG_WARN_EMPTY_BODY = YES; 331 | CLANG_WARN_ENUM_CONVERSION = YES; 332 | CLANG_WARN_INFINITE_RECURSION = YES; 333 | CLANG_WARN_INT_CONVERSION = YES; 334 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 335 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 336 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 337 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 338 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 339 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 340 | CLANG_WARN_STRICT_PROTOTYPES = YES; 341 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 342 | CLANG_WARN_UNREACHABLE_CODE = YES; 343 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 344 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 345 | COPY_PHASE_STRIP = NO; 346 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | ENABLE_TESTABILITY = YES; 349 | GCC_C_LANGUAGE_STANDARD = gnu99; 350 | GCC_DYNAMIC_NO_PIC = NO; 351 | GCC_NO_COMMON_BLOCKS = YES; 352 | GCC_OPTIMIZATION_LEVEL = 0; 353 | GCC_PREPROCESSOR_DEFINITIONS = ( 354 | "DEBUG=1", 355 | "$(inherited)", 356 | ); 357 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 360 | GCC_WARN_UNDECLARED_SELECTOR = YES; 361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 362 | GCC_WARN_UNUSED_FUNCTION = YES; 363 | GCC_WARN_UNUSED_VARIABLE = YES; 364 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 365 | MTL_ENABLE_DEBUG_INFO = YES; 366 | ONLY_ACTIVE_ARCH = YES; 367 | SDKROOT = iphoneos; 368 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 369 | }; 370 | name = Debug; 371 | }; 372 | 607FACEE1AFB9204008FA782 /* Release */ = { 373 | isa = XCBuildConfiguration; 374 | buildSettings = { 375 | ALWAYS_SEARCH_USER_PATHS = NO; 376 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 377 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 378 | CLANG_CXX_LIBRARY = "libc++"; 379 | CLANG_ENABLE_MODULES = YES; 380 | CLANG_ENABLE_OBJC_ARC = YES; 381 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 382 | CLANG_WARN_BOOL_CONVERSION = YES; 383 | CLANG_WARN_COMMA = YES; 384 | CLANG_WARN_CONSTANT_CONVERSION = YES; 385 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INFINITE_RECURSION = YES; 390 | CLANG_WARN_INT_CONVERSION = YES; 391 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 393 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 395 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 396 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 397 | CLANG_WARN_STRICT_PROTOTYPES = YES; 398 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 402 | COPY_PHASE_STRIP = NO; 403 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 404 | ENABLE_NS_ASSERTIONS = NO; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | GCC_C_LANGUAGE_STANDARD = gnu99; 407 | GCC_NO_COMMON_BLOCKS = YES; 408 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 409 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 410 | GCC_WARN_UNDECLARED_SELECTOR = YES; 411 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 412 | GCC_WARN_UNUSED_FUNCTION = YES; 413 | GCC_WARN_UNUSED_VARIABLE = YES; 414 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 415 | MTL_ENABLE_DEBUG_INFO = NO; 416 | SDKROOT = iphoneos; 417 | SWIFT_COMPILATION_MODE = wholemodule; 418 | VALIDATE_PRODUCT = YES; 419 | }; 420 | name = Release; 421 | }; 422 | 607FACF01AFB9204008FA782 /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 426 | CLANG_ENABLE_MODULES = YES; 427 | DEVELOPMENT_TEAM = NR8DLKJ7E6; 428 | INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist"; 429 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 430 | LD_RUNPATH_SEARCH_PATHS = ( 431 | "$(inherited)", 432 | "@executable_path/Frameworks", 433 | ); 434 | MODULE_NAME = ExampleApp; 435 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 436 | PRODUCT_NAME = NukeDemo_Example; 437 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 438 | SWIFT_VERSION = 5.0; 439 | TARGETED_DEVICE_FAMILY = "1,2"; 440 | }; 441 | name = Debug; 442 | }; 443 | 607FACF11AFB9204008FA782 /* Release */ = { 444 | isa = XCBuildConfiguration; 445 | buildSettings = { 446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 447 | CLANG_ENABLE_MODULES = YES; 448 | DEVELOPMENT_TEAM = NR8DLKJ7E6; 449 | INFOPLIST_FILE = "$(SRCROOT)/Resources/Info.plist"; 450 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 451 | LD_RUNPATH_SEARCH_PATHS = ( 452 | "$(inherited)", 453 | "@executable_path/Frameworks", 454 | ); 455 | MODULE_NAME = ExampleApp; 456 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 457 | PRODUCT_NAME = NukeDemo_Example; 458 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 459 | SWIFT_VERSION = 5.0; 460 | TARGETED_DEVICE_FAMILY = "1,2"; 461 | }; 462 | name = Release; 463 | }; 464 | /* End XCBuildConfiguration section */ 465 | 466 | /* Begin XCConfigurationList section */ 467 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "NukeDemo" */ = { 468 | isa = XCConfigurationList; 469 | buildConfigurations = ( 470 | 607FACED1AFB9204008FA782 /* Debug */, 471 | 607FACEE1AFB9204008FA782 /* Release */, 472 | ); 473 | defaultConfigurationIsVisible = 0; 474 | defaultConfigurationName = Release; 475 | }; 476 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "NukeDemo" */ = { 477 | isa = XCConfigurationList; 478 | buildConfigurations = ( 479 | 607FACF01AFB9204008FA782 /* Debug */, 480 | 607FACF11AFB9204008FA782 /* Release */, 481 | ); 482 | defaultConfigurationIsVisible = 0; 483 | defaultConfigurationName = Release; 484 | }; 485 | /* End XCConfigurationList section */ 486 | 487 | /* Begin XCRemoteSwiftPackageReference section */ 488 | 0C880533242EB72E00F8C5B3 /* XCRemoteSwiftPackageReference "SwiftSVG" */ = { 489 | isa = XCRemoteSwiftPackageReference; 490 | repositoryURL = "https://github.com/mchoe/SwiftSVG.git"; 491 | requirement = { 492 | kind = upToNextMajorVersion; 493 | minimumVersion = 2.3.2; 494 | }; 495 | }; 496 | 0C895237288A07A3003BC1C2 /* XCRemoteSwiftPackageReference "Alamofire" */ = { 497 | isa = XCRemoteSwiftPackageReference; 498 | repositoryURL = "https://github.com/Alamofire/Alamofire"; 499 | requirement = { 500 | kind = upToNextMajorVersion; 501 | minimumVersion = 5.0.0; 502 | }; 503 | }; 504 | 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */ = { 505 | isa = XCRemoteSwiftPackageReference; 506 | repositoryURL = "https://github.com/kean/Nuke"; 507 | requirement = { 508 | branch = main; 509 | kind = branch; 510 | }; 511 | }; 512 | 0CC841A129CF3CCA0042A3A4 /* XCRemoteSwiftPackageReference "Gifu" */ = { 513 | isa = XCRemoteSwiftPackageReference; 514 | repositoryURL = "https://github.com/kaishin/Gifu"; 515 | requirement = { 516 | kind = upToNextMajorVersion; 517 | minimumVersion = 3.0.0; 518 | }; 519 | }; 520 | 0CCF787B28A03BDC00C431E5 /* XCRemoteSwiftPackageReference "Pulse" */ = { 521 | isa = XCRemoteSwiftPackageReference; 522 | repositoryURL = "https://github.com/kean/Pulse"; 523 | requirement = { 524 | branch = main; 525 | kind = branch; 526 | }; 527 | }; 528 | /* End XCRemoteSwiftPackageReference section */ 529 | 530 | /* Begin XCSwiftPackageProductDependency section */ 531 | 0C880534242EB72E00F8C5B3 /* SwiftSVG */ = { 532 | isa = XCSwiftPackageProductDependency; 533 | package = 0C880533242EB72E00F8C5B3 /* XCRemoteSwiftPackageReference "SwiftSVG" */; 534 | productName = SwiftSVG; 535 | }; 536 | 0C895238288A07A3003BC1C2 /* Alamofire */ = { 537 | isa = XCSwiftPackageProductDependency; 538 | package = 0C895237288A07A3003BC1C2 /* XCRemoteSwiftPackageReference "Alamofire" */; 539 | productName = Alamofire; 540 | }; 541 | 0C9E5FB928AC4AF600FC1320 /* Nuke */ = { 542 | isa = XCSwiftPackageProductDependency; 543 | package = 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */; 544 | productName = Nuke; 545 | }; 546 | 0C9E5FBB28AC4AF600FC1320 /* NukeExtensions */ = { 547 | isa = XCSwiftPackageProductDependency; 548 | package = 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */; 549 | productName = NukeExtensions; 550 | }; 551 | 0C9E5FBD28AC4AF600FC1320 /* NukeUI */ = { 552 | isa = XCSwiftPackageProductDependency; 553 | package = 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */; 554 | productName = NukeUI; 555 | }; 556 | 0C9E5FBF28AC4B0200FC1320 /* Pulse */ = { 557 | isa = XCSwiftPackageProductDependency; 558 | package = 0CCF787B28A03BDC00C431E5 /* XCRemoteSwiftPackageReference "Pulse" */; 559 | productName = Pulse; 560 | }; 561 | 0CC841A229CF3CCA0042A3A4 /* Gifu */ = { 562 | isa = XCSwiftPackageProductDependency; 563 | package = 0CC841A129CF3CCA0042A3A4 /* XCRemoteSwiftPackageReference "Gifu" */; 564 | productName = Gifu; 565 | }; 566 | 0CC841A429CF4ADF0042A3A4 /* NukeVideo */ = { 567 | isa = XCSwiftPackageProductDependency; 568 | package = 0C9E5FB828AC4AF600FC1320 /* XCRemoteSwiftPackageReference "Nuke" */; 569 | productName = NukeVideo; 570 | }; 571 | 0CCF787E28A03BDC00C431E5 /* PulseUI */ = { 572 | isa = XCSwiftPackageProductDependency; 573 | package = 0CCF787B28A03BDC00C431E5 /* XCRemoteSwiftPackageReference "Pulse" */; 574 | productName = PulseUI; 575 | }; 576 | /* End XCSwiftPackageProductDependency section */ 577 | }; 578 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 579 | } 580 | -------------------------------------------------------------------------------- /NukeDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NukeDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NukeDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "2f456449c38e4813f4b9b3fdfc018ebed1c3bada5f3159b077c9f36e84e0300b", 3 | "pins" : [ 4 | { 5 | "identity" : "alamofire", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Alamofire/Alamofire", 8 | "state" : { 9 | "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", 10 | "version" : "5.9.1" 11 | } 12 | }, 13 | { 14 | "identity" : "gifu", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/kaishin/Gifu", 17 | "state" : { 18 | "revision" : "82da0086dea14ca9afc9801234ad8dc4cd9e2738", 19 | "version" : "3.4.1" 20 | } 21 | }, 22 | { 23 | "identity" : "nuke", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/kean/Nuke", 26 | "state" : { 27 | "branch" : "main", 28 | "revision" : "084076db4f9b9ee4db51177ef5a4879689c1aa09" 29 | } 30 | }, 31 | { 32 | "identity" : "pulse", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/kean/Pulse", 35 | "state" : { 36 | "branch" : "main", 37 | "revision" : "578df0540e91041f2720afbd50e2550474e4e7af" 38 | } 39 | }, 40 | { 41 | "identity" : "swiftsvg", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/mchoe/SwiftSVG.git", 44 | "state" : { 45 | "revision" : "c3a8866a25ace169ee8e5c037670ba14aa59606f", 46 | "version" : "2.3.2" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /NukeDemo.xcodeproj/xcshareddata/xcschemes/NukeDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuke Demo 2 | 3 | To run the demo, open the Xcode project from the repo, and run it. 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Resources/IB/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Resources/IB/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | } 153 | ], 154 | "info" : { 155 | "author" : "xcode", 156 | "version" : 1 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Resources/Images.xcassets/nuke-logo.imageset/114792417-57c1d080-9d56-11eb-8035-dc07cfd7557f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/Images.xcassets/nuke-logo.imageset/114792417-57c1d080-9d56-11eb-8035-dc07cfd7557f.png -------------------------------------------------------------------------------- /Resources/Images.xcassets/nuke-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "114792417-57c1d080-9d56-11eb-8035-dc07cfd7557f.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Nuke Demo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Resources/baseline.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/baseline.jpeg -------------------------------------------------------------------------------- /Resources/progressive.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kean/NukeDemo/621b2d2ef8dbf2b96db7e0931dda6f5b628fbd8a/Resources/progressive.jpeg -------------------------------------------------------------------------------- /Sources/AlamofireIntegrationDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | @preconcurrency import Alamofire 8 | 9 | final class AlamofireIntegrationDemoViewController: BasicDemoViewController { 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | pipeline = ImagePipeline { 14 | $0.dataLoader = AlamofireDataLoader() 15 | $0.imageCache = ImageCache() 16 | } 17 | } 18 | } 19 | 20 | /// Implements data loading using Alamofire framework. 21 | public final class AlamofireDataLoader: Nuke.DataLoading { 22 | public let session: Alamofire.Session 23 | 24 | /// Initializes the receiver with a given Alamofire.SessionManager. 25 | /// - parameter session: Alamofire.Session.default by default. 26 | public init(session: Alamofire.Session = Alamofire.Session.default) { 27 | self.session = session 28 | } 29 | 30 | // MARK: DataLoading 31 | 32 | /// Loads data using Alamofire.SessionManager. 33 | public func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { 34 | // Alamofire.SessionManager automatically starts requests as soon as they are created (see `startRequestsImmediately`) 35 | let task = self.session.streamRequest(request) 36 | task.responseStream { [weak task] stream in 37 | switch stream.event { 38 | case let .stream(result): 39 | switch result { 40 | case let .success(data): 41 | guard let response = task?.response else { return } // Never nil 42 | didReceiveData(data, response) 43 | } 44 | case let .complete(response): 45 | completion(response.error) 46 | } 47 | } 48 | return AnyCancellable { 49 | task.cancel() 50 | } 51 | } 52 | 53 | public func removeData(for request: URLRequest) { 54 | // Do nothing 55 | } 56 | } 57 | 58 | private final class AnyCancellable: Nuke.Cancellable { 59 | let closure: @Sendable () -> Void 60 | 61 | init(_ closure: @Sendable @escaping () -> Void) { 62 | self.closure = closure 63 | } 64 | 65 | func cancel() { 66 | closure() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AnimatedImageUsingVideoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import NukeUI 8 | import NukeVideo 9 | import AVFoundation 10 | 11 | final class AnimatedImageUsingVideoViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { 12 | private let pipeline = ImagePipeline { 13 | $0.makeImageDecoder = { 14 | ImageDecoders.Video(context: $0) 15 | 16 | // Use ImageDecoderRegistory to add the decoder to the 17 | // ImageDecoderRegistry.shared.register(ImageDecoders.Video.init) 18 | } 19 | } 20 | 21 | override init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) { 22 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | collectionView?.register(VideoCell.self, forCellWithReuseIdentifier: imageCellReuseID) 33 | collectionView.backgroundColor = UIColor.systemBackground 34 | 35 | let layout = collectionViewLayout as! UICollectionViewFlowLayout 36 | layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 37 | layout.minimumInteritemSpacing = 8 38 | } 39 | 40 | // MARK: Collection View 41 | 42 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 43 | return 1 44 | } 45 | 46 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 47 | return imageURLs.count 48 | } 49 | 50 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 51 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellReuseID, for: indexPath) as! VideoCell 52 | cell.imageView.pipeline = pipeline 53 | cell.setVideo(with: imageURLs[indexPath.row]) 54 | return cell 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 58 | let layout = collectionViewLayout as! UICollectionViewFlowLayout 59 | let width = view.bounds.size.width - layout.sectionInset.left - layout.sectionInset.right 60 | return CGSize(width: width, height: width) 61 | } 62 | } 63 | 64 | private let imageCellReuseID = "imageCellReuseID" 65 | 66 | private let imageURLs = [ 67 | URL(string: "https://kean.github.io/videos/cat_video.mp4")! 68 | ] 69 | 70 | // MARK: - VideoCell 71 | 72 | /// - warning: This is proof of concept, please don't use in production. 73 | private final class VideoCell: UICollectionViewCell { 74 | let imageView = LazyImageView() 75 | 76 | deinit { 77 | prepareForReuse() 78 | } 79 | 80 | override init(frame: CGRect) { 81 | super.init(frame: frame) 82 | 83 | backgroundColor = UIColor(white: 235.0 / 255.0, alpha: 1.0) 84 | 85 | imageView.makeImageView = { container in 86 | if let type = container.type, type.isVideo, let asset = container.userInfo[.videoAssetKey] as? AVAsset { 87 | let view = VideoPlayerView() 88 | view.asset = asset 89 | view.play() 90 | return view 91 | } 92 | return nil 93 | } 94 | 95 | contentView.addSubview(imageView) 96 | imageView.pinToSuperview() 97 | 98 | imageView.placeholderView = UIActivityIndicatorView(style: .medium) 99 | } 100 | 101 | required init?(coder aDecoder: NSCoder) { 102 | fatalError() 103 | } 104 | 105 | override func prepareForReuse() { 106 | super.prepareForReuse() 107 | 108 | imageView.reset() 109 | } 110 | 111 | func setVideo(with url: URL) { 112 | imageView.url = url 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | func applicationDidFinishLaunching(_ application: UIApplication) { 14 | window?.tintColor = UIColor.systemPink 15 | ImagePipeline.Configuration.isSignpostLoggingEnabled = true 16 | 17 | if #available(iOS 16, *) { 18 | print(URL.cachesDirectory) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/BasicDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import NukeExtensions 8 | 9 | /// A base view controller. 10 | class BasicDemoViewController: UICollectionViewController, ImagePipelineSettingsViewControllerDelegate { 11 | var photos: [URL] = [] 12 | var pipeline = ImagePipeline.shared 13 | var itemsPerRow: Int = 4 14 | 15 | init(collectionViewLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()) { 16 | super.init(collectionViewLayout: collectionViewLayout) 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | photos = demoPhotosURLs 27 | 28 | collectionView?.backgroundColor = UIColor.systemBackground 29 | collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellReuseID) 30 | 31 | collectionView?.refreshControl = UIRefreshControl() 32 | collectionView?.refreshControl?.addTarget(self, action: #selector(refreshControlValueChanged), for: .valueChanged) 33 | 34 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Configuration", style: .plain, target: self, action: #selector(buttonShowSettingsTapped)) 35 | } 36 | 37 | override func viewWillAppear(_ animated: Bool) { 38 | super.viewWillAppear(animated) 39 | updateItemSize() 40 | } 41 | 42 | override func viewDidLayoutSubviews() { 43 | super.viewDidLayoutSubviews() 44 | updateItemSize() 45 | } 46 | 47 | func updateItemSize() { 48 | let layout = collectionViewLayout as! UICollectionViewFlowLayout 49 | layout.minimumLineSpacing = 2.0 50 | layout.minimumInteritemSpacing = 2.0 51 | let side = (Double(view.bounds.size.width) - Double(itemsPerRow - 1) * 2.0) / Double(itemsPerRow) 52 | layout.itemSize = CGSize(width: side, height: side) 53 | } 54 | 55 | // MARK: UICollectionView 56 | 57 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 58 | photos.count 59 | } 60 | 61 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 62 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseID, for: indexPath) 63 | cell.backgroundColor = UIColor.secondarySystemBackground 64 | 65 | let imageView = imageViewForCell(cell) 66 | let request = makeRequest(with: photos[indexPath.row], cellSize: cell.bounds.size) 67 | var options = makeImageLoadingOptions() 68 | options.pipeline = self.pipeline 69 | NukeExtensions.loadImage(with: request, options: options, into: imageView) 70 | 71 | return cell 72 | } 73 | 74 | func makeRequest(with url: URL, cellSize: CGSize) -> ImageRequest { 75 | ImageRequest(url: url) 76 | } 77 | 78 | func makeImageLoadingOptions() -> ImageLoadingOptions { 79 | ImageLoadingOptions(transition: .fadeIn(duration: 0.25)) 80 | } 81 | 82 | func imageViewForCell(_ cell: UICollectionViewCell) -> UIImageView { 83 | var imageView: UIImageView! = cell.viewWithTag(15) as? UIImageView 84 | if imageView == nil { 85 | imageView = UIImageView() 86 | imageView.tag = 15 87 | imageView.contentMode = .scaleAspectFill 88 | imageView.clipsToBounds = true 89 | cell.addSubview(imageView) 90 | imageView.pinToSuperview() 91 | } 92 | return imageView! 93 | } 94 | 95 | // MARK: - Actions 96 | 97 | @objc func refreshControlValueChanged() { 98 | collectionView.reloadData() 99 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 100 | self.collectionView?.refreshControl?.endRefreshing() 101 | } 102 | } 103 | 104 | @objc func buttonShowSettingsTapped() { 105 | ImagePipelineSettingsViewController.show(from: self, pipeline: pipeline) 106 | } 107 | 108 | // MARK: - ImagePipelineSettingsViewControllerDelegate 109 | 110 | func imagePipelineSettingsViewController(_ vc: ImagePipelineSettingsViewController, didFinishWithConfiguration configuration: ImagePipeline.Configuration) { 111 | self.pipeline = ImagePipeline(configuration: configuration) 112 | vc.dismiss(animated: true) {} 113 | } 114 | } 115 | 116 | private let cellReuseID = "reuseID" 117 | -------------------------------------------------------------------------------- /Sources/DataCachingDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | 8 | final class DataCachingDemoViewController: BasicDemoViewController { 9 | override func viewDidLoad() { 10 | super.viewDidLoad() 11 | 12 | pipeline = ImagePipeline { 13 | $0.dataLoader = DataLoader(configuration: { 14 | // Disable disk caching built into URLSession 15 | let conf = DataLoader.defaultConfiguration 16 | conf.urlCache = nil 17 | return conf 18 | }()) 19 | 20 | $0.imageCache = ImageCache() 21 | $0.dataCache = try! DataCache(name: "com.github.kean.Nuke.DataCache") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/GifuDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import NukeUI 7 | import Gifu 8 | 9 | final class GifuDemoViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout { 10 | override init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) { 11 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 12 | } 13 | 14 | required init?(coder aDecoder: NSCoder) { 15 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 16 | } 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | collectionView?.register(AnimatedImageCell.self, forCellWithReuseIdentifier: imageCellReuseID) 22 | collectionView?.backgroundColor = UIColor.systemBackground 23 | 24 | let layout = collectionViewLayout as! UICollectionViewFlowLayout 25 | layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) 26 | layout.minimumInteritemSpacing = 8 27 | } 28 | 29 | // MARK: Collection View 30 | 31 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 32 | 1 33 | } 34 | 35 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 36 | switch section { 37 | default: return imageURLs.count 38 | } 39 | } 40 | 41 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 42 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellReuseID, for: indexPath) as! AnimatedImageCell 43 | cell.setImage(with: imageURLs[indexPath.row]) 44 | return cell 45 | } 46 | 47 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 48 | let layout = collectionViewLayout as! UICollectionViewFlowLayout 49 | let width = view.bounds.size.width - layout.sectionInset.left - layout.sectionInset.right 50 | return CGSize(width: width, height: width) 51 | } 52 | } 53 | 54 | private let imageCellReuseID = "imageCellReuseID" 55 | 56 | // MARK: - Image URLs 57 | 58 | private let root = "https://cloud.githubusercontent.com/assets" 59 | private let imageURLs = [ 60 | URL(string: "\(root)/1567433/6505557/77ff05ac-c2e7-11e4-9a09-ce5b7995cad0.gif")!, 61 | URL(string: "\(root)/1567433/6505565/8aa02c90-c2e7-11e4-8127-71df010ca06d.gif")!, 62 | URL(string: "\(root)/1567433/6505571/a28a6e2e-c2e7-11e4-8161-9f39cc3bb8df.gif")!, 63 | URL(string: "\(root)/1567433/6505576/b785a8ac-c2e7-11e4-831a-666e2b064b95.gif")!, 64 | URL(string: "\(root)/1567433/6505579/c88c77ca-c2e7-11e4-88ad-d98c7360602d.gif")!, 65 | URL(string: "\(root)/1567433/6505595/def06c06-c2e7-11e4-9cdf-d37d28618af0.gif")!, 66 | URL(string: "\(root)/1567433/6505634/26e5dad2-c2e8-11e4-89c3-3c3a63110ac0.gif")!, 67 | URL(string: "\(root)/1567433/6505643/42eb3ee8-c2e8-11e4-8666-ac9c8e1dc9b5.gif")! 68 | ] 69 | 70 | // MARK: - AnimatedImageCell 71 | 72 | private final class AnimatedImageCell: UICollectionViewCell { 73 | private let imageView: LazyImageView 74 | private let spinner: UIActivityIndicatorView 75 | 76 | override init(frame: CGRect) { 77 | imageView = LazyImageView() 78 | imageView.makeImageView = { container in 79 | if container.type == .gif, let data = container.data { 80 | let view = GIFImageView() 81 | view.animate(withGIFData: data) 82 | return view 83 | } 84 | return nil 85 | } 86 | imageView.placeholderView = UIActivityIndicatorView(style: .medium) 87 | imageView.contentMode = .scaleAspectFill 88 | imageView.clipsToBounds = true 89 | 90 | spinner = UIActivityIndicatorView(style: .medium) 91 | 92 | super.init(frame: frame) 93 | 94 | backgroundColor = UIColor(white: 235.0 / 255.0, alpha: 1.0) 95 | 96 | contentView.addSubview(imageView) 97 | contentView.addSubview(spinner) 98 | 99 | imageView.pinToSuperview() 100 | spinner.centerInSuperview() 101 | } 102 | 103 | required init?(coder aDecoder: NSCoder) { 104 | fatalError() 105 | } 106 | 107 | func setImage(with url: URL) { 108 | imageView.url = url 109 | } 110 | 111 | override func prepareForReuse() { 112 | super.prepareForReuse() 113 | 114 | imageView.reset() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Helpers/GIFImage.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import SwiftUI 6 | import Gifu 7 | 8 | public struct GIFImage: View { 9 | private let source: GIFSource 10 | private var loopCount = 0 11 | private var isResizable = false 12 | 13 | /// Initializes the view with the given GIF image data. 14 | public init(data: Data) { 15 | self.source = .data(data) 16 | } 17 | 18 | /// Initialzies the view with the given GIF image url. 19 | public init(url: URL) { 20 | self.source = .url(url) 21 | } 22 | 23 | /// Initialzies the view with the given GIF image name. 24 | public init(imageName: String) { 25 | self.source = .imageName(imageName) 26 | } 27 | 28 | /// Sets the desired number of loops. By default, the number of loops infinite. 29 | public func loopCount(_ value: Int) -> GIFImage { 30 | var copy = self 31 | copy.loopCount = value 32 | return copy 33 | } 34 | 35 | /// Sets an image to fit its space. 36 | public func resizable() -> GIFImage { 37 | var copy = self 38 | copy.isResizable = true 39 | return copy 40 | } 41 | 42 | public var body: some View { 43 | _GIFImage(source: source, loopCount: loopCount, isResizable: isResizable) 44 | } 45 | } 46 | 47 | @available(iOS 13, tvOS 13, *) 48 | private struct _GIFImage: UIViewRepresentable { 49 | let source: GIFSource 50 | let loopCount: Int 51 | let isResizable: Bool 52 | 53 | func makeUIView(context: Context) -> GIFImageView { 54 | let imageView = GIFImageView() 55 | if isResizable { 56 | imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 57 | imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) 58 | } 59 | return imageView 60 | } 61 | 62 | func updateUIView(_ imageView: GIFImageView, context: Context) { 63 | switch source { 64 | case .data(let data): 65 | imageView.animate(withGIFData: data, loopCount: loopCount) 66 | case .url(let url): 67 | imageView.animate(withGIFURL: url, loopCount: loopCount) 68 | case .imageName(let imageName): 69 | imageView.animate(withGIFNamed: imageName, loopCount: loopCount) 70 | } 71 | } 72 | 73 | static func dismantleUIView(_ imageView: GIFImageView, coordinator: ()) { 74 | imageView.prepareForReuse() 75 | } 76 | } 77 | 78 | private enum GIFSource { 79 | case data(Data) 80 | case url(URL) 81 | case imageName(String) 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Helpers/Images.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | 7 | private let host = "https://cloud.githubusercontent.com/assets" 8 | let demoPhotosURLs = [ 9 | URL(string: "\(host)/1567433/9781817/ecb16e82-57a0-11e5-9b43-6b4f52659997.jpg")!, 10 | URL(string: "\(host)/1567433/9781832/0719dd5e-57a1-11e5-9324-9764de25ed47.jpg")!, 11 | URL(string: "\(host)/1567433/9781833/09021316-57a1-11e5-817b-85b57a2a8a77.jpg")!, 12 | URL(string: "\(host)/1567433/9781834/0931ad74-57a1-11e5-9080-c8f6ecea19ce.jpg")!, 13 | URL(string: "\(host)/1567433/9781838/0e6274f4-57a1-11e5-82fd-872e735eea73.jpg")!, 14 | URL(string: "\(host)/1567433/9781839/0e63ad92-57a1-11e5-8841-bd3c5ea1bb9c.jpg")!, 15 | URL(string: "\(host)/1567433/9781843/0f4064b2-57a1-11e5-9fb7-f258e81a4214.jpg")!, 16 | URL(string: "\(host)/1567433/9781840/0e95f978-57a1-11e5-8179-36dfed72f985.jpg")!, 17 | URL(string: "\(host)/1567433/9781841/0e96b5fc-57a1-11e5-82ae-699b113bb85a.jpg")!, 18 | URL(string: "\(host)/1567433/9781894/839cf99c-57a1-11e5-9602-d56d99a31abc.jpg")!, 19 | URL(string: "\(host)/1567433/9781896/83c5e1f4-57a1-11e5-9961-97730da2a7ad.jpg")!, 20 | URL(string: "\(host)/1567433/9781897/83c622cc-57a1-11e5-98dd-3a7d54b60170.jpg")!, 21 | URL(string: "\(host)/1567433/9781900/83cbc934-57a1-11e5-8152-e9ecab92db75.jpg")!, 22 | URL(string: "\(host)/1567433/9781899/83cb13a4-57a1-11e5-88c4-48feb134a9f0.jpg")!, 23 | URL(string: "\(host)/1567433/9781898/83c85ba0-57a1-11e5-8569-778689bff1ed.jpg")!, 24 | URL(string: "\(host)/1567433/9781895/83b7f3fa-57a1-11e5-8579-e2fd6098052d.jpg")!, 25 | URL(string: "\(host)/1567433/9781901/83d5d500-57a1-11e5-9894-78467657874c.jpg")!, 26 | URL(string: "\(host)/1567433/9781902/83df3b72-57a1-11e5-82b0-e6eb08915402.jpg")!, 27 | URL(string: "\(host)/1567433/9781903/83e400bc-57a1-11e5-881d-c0ed2c5136f6.jpg")!, 28 | URL(string: "\(host)/1567433/9781964/f4553bea-57a1-11e5-9abf-f23470a5efc1.jpg")!, 29 | URL(string: "\(host)/1567433/9781955/f3b2ed18-57a1-11e5-8fc7-0579e44de0b0.jpg")!, 30 | URL(string: "\(host)/1567433/9781959/f3b7e624-57a1-11e5-8982-8017f53a4898.jpg")!, 31 | URL(string: "\(host)/1567433/9781957/f3b52e98-57a1-11e5-9f1a-8741acddb12d.jpg")!, 32 | URL(string: "\(host)/1567433/9781958/f3b5544a-57a1-11e5-880a-478507b2e189.jpg")!, 33 | URL(string: "\(host)/1567433/9781956/f3b35082-57a1-11e5-9d2f-2c364e3f9b68.jpg")!, 34 | URL(string: "\(host)/1567433/9781963/f3da11b8-57a1-11e5-838e-c75e6b00f33e.jpg")!, 35 | URL(string: "\(host)/1567433/9781961/f3d865de-57a1-11e5-87fd-bb8f28515a16.jpg")!, 36 | URL(string: "\(host)/1567433/9781960/f3d7f306-57a1-11e5-833f-f3802344619e.jpg")!, 37 | URL(string: "\(host)/1567433/9781962/f3d98c20-57a1-11e5-838e-10f9d20fbc9b.jpg")!, 38 | URL(string: "\(host)/1567433/9781982/2b67875a-57a2-11e5-91b2-ec4ca2a65674.jpg")!, 39 | URL(string: "\(host)/1567433/9781985/2b92e576-57a2-11e5-955f-73889423b552.jpg")!, 40 | URL(string: "\(host)/1567433/9781986/2b94c288-57a2-11e5-8ebd-4cc107444e70.jpg")!, 41 | URL(string: "\(host)/1567433/9781987/2b94ba72-57a2-11e5-8259-8d4b5fce1f6c.jpg")!, 42 | URL(string: "\(host)/1567433/9781984/2b9244ea-57a2-11e5-89b1-edc6922d1909.jpg")!, 43 | URL(string: "\(host)/1567433/9781988/2b94f32a-57a2-11e5-94f6-2c68c15f711f.jpg")!, 44 | URL(string: "\(host)/1567433/9781983/2b80e9ca-57a2-11e5-9a90-54884428affe.jpg")!, 45 | URL(string: "\(host)/1567433/9781989/2b9d462e-57a2-11e5-8c5c-d005e79e0070.jpg")!, 46 | URL(string: "\(host)/1567433/9781990/2babeeae-57a2-11e5-828d-6c050683274d.jpg")!, 47 | URL(string: "\(host)/1567433/9781991/2bb13a94-57a2-11e5-8a70-1d7e519c1631.jpg")!, 48 | URL(string: "\(host)/1567433/9781992/2bb2161c-57a2-11e5-8715-9b7d2df58708.jpg")!, 49 | URL(string: "\(host)/1567433/9781993/2bb397a8-57a2-11e5-853d-4d4f1854d1fe.jpg")!, 50 | URL(string: "\(host)/1567433/9781994/2bb61e88-57a2-11e5-8e45-bc2ed096cf97.jpg")!, 51 | URL(string: "\(host)/1567433/9781995/2bbdf73e-57a2-11e5-8847-afb709e28495.jpg")!, 52 | URL(string: "\(host)/1567433/9781996/2bc90a66-57a2-11e5-9154-6cc3a08a3e93.jpg")!, 53 | URL(string: "\(host)/1567433/9782000/2bd232a8-57a2-11e5-8617-eaff327b927f.jpg")!, 54 | URL(string: "\(host)/1567433/9781997/2bced964-57a2-11e5-9021-970f1f92608e.jpg")!, 55 | URL(string: "\(host)/1567433/9781998/2bd0def8-57a2-11e5-850f-e60701db4f62.jpg")!, 56 | URL(string: "\(host)/1567433/9781999/2bd2551c-57a2-11e5-82e3-54bb80f7c114.jpg")!, 57 | URL(string: "\(host)/1567433/9782001/2bdb5bb2-57a2-11e5-8a18-05fe673e2315.jpg")!, 58 | URL(string: "\(host)/1567433/9782002/2be52ed0-57a2-11e5-8e12-2f6e17787553.jpg")!, 59 | URL(string: "\(host)/1567433/9782003/2bed36de-57a2-11e5-9d4f-7c214e828fe6.jpg")!, 60 | URL(string: "\(host)/1567433/9782004/2bef8ed4-57a2-11e5-8949-26e1b80a0ebb.jpg")!, 61 | URL(string: "\(host)/1567433/9782005/2bf08622-57a2-11e5-86e2-c5d71ef615e9.jpg")!, 62 | URL(string: "\(host)/1567433/9782006/2bf2d968-57a2-11e5-8f44-3cd169219e78.jpg")!, 63 | URL(string: "\(host)/1567433/9782007/2bf5e95a-57a2-11e5-9b7a-96f355a5334b.jpg")!, 64 | URL(string: "\(host)/1567433/9782008/2c04b458-57a2-11e5-9381-feb4ae365a1d.jpg")!, 65 | URL(string: "\(host)/1567433/9782011/2c0e4054-57a2-11e5-89f0-7c91bb0e01a2.jpg")!, 66 | URL(string: "\(host)/1567433/9782009/2c0c4254-57a2-11e5-984d-0e44cc762219.jpg")!, 67 | URL(string: "\(host)/1567433/9782010/2c0ca730-57a2-11e5-834c-79153b496d44.jpg")!, 68 | URL(string: "\(host)/1567433/9782012/2c1277e6-57a2-11e5-862a-ec0c8fad727a.jpg")!, 69 | URL(string: "\(host)/1567433/9782122/543bc690-57a3-11e5-83eb-156108681377.jpg")!, 70 | URL(string: "\(host)/1567433/9782128/546af1f4-57a3-11e5-8ad6-78527accf642.jpg")!, 71 | URL(string: "\(host)/1567433/9782127/546ae2cc-57a3-11e5-9ad5-f0c7157eda5b.jpg")!, 72 | URL(string: "\(host)/1567433/9782124/5468528c-57a3-11e5-9cf9-89f763b473b4.jpg")!, 73 | URL(string: "\(host)/1567433/9782126/5468cf50-57a3-11e5-9d97-c8fc94e7b9a4.jpg")!, 74 | URL(string: "\(host)/1567433/9782125/54687d66-57a3-11e5-860f-c66597fd212c.jpg")!, 75 | URL(string: "\(host)/1567433/9782123/545728cc-57a3-11e5-83ab-51462737c19d.jpg")!, 76 | URL(string: "\(host)/1567433/9782129/54737694-57a3-11e5-9e1e-b626db67e625.jpg")!, 77 | URL(string: "\(host)/1567433/9782130/5483fee2-57a3-11e5-8928-e7706c765016.jpg")!, 78 | URL(string: "\(host)/1567433/9782133/54dd0c62-57a3-11e5-85ee-a02c1b9dd223.jpg")!, 79 | URL(string: "\(host)/1567433/9782131/54872b30-57a3-11e5-8903-db1f81ea1abb.jpg")!, 80 | URL(string: "\(host)/1567433/9782132/548a3b9a-57a3-11e5-8228-8ee523e7809e.jpg")! 81 | ] 82 | -------------------------------------------------------------------------------- /Sources/Helpers/Utilities.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | 8 | extension UIView { 9 | func pinToSuperview() { 10 | translatesAutoresizingMaskIntoConstraints = false 11 | NSLayoutConstraint.activate([ 12 | topAnchor.constraint(equalTo: superview!.topAnchor), 13 | bottomAnchor.constraint(equalTo: superview!.bottomAnchor), 14 | leftAnchor.constraint(equalTo: superview!.leftAnchor), 15 | rightAnchor.constraint(equalTo: superview!.rightAnchor) 16 | ]) 17 | } 18 | 19 | func centerInSuperview() { 20 | translatesAutoresizingMaskIntoConstraints = false 21 | NSLayoutConstraint.activate([ 22 | centerXAnchor.constraint(equalTo: superview!.centerXAnchor), 23 | centerYAnchor.constraint(equalTo: superview!.centerYAnchor) 24 | ]) 25 | } 26 | } 27 | 28 | typealias DataLoader = Nuke.DataLoader 29 | 30 | // MARK: Core Image Integrations 31 | 32 | /// Blurs image using CIGaussianBlur filter. Only blurs first scans of the 33 | /// progressive JPEG. 34 | struct _ProgressiveBlurImageProcessor: ImageProcessing, Hashable { 35 | func process(_ image: PlatformImage) -> PlatformImage? { 36 | return image 37 | } 38 | 39 | func process(_ container: ImageContainer, context: ImageProcessingContext) -> ImageContainer? { 40 | // CoreImage is too slow on simulator. 41 | #if targetEnvironment(simulator) 42 | return container 43 | #else 44 | guard !context.isFinal else { 45 | return container // No processing. 46 | } 47 | 48 | guard let scanNumber = container.userInfo[.scanNumberKey] as? Int else { 49 | return container 50 | } 51 | 52 | // Blur partial images. 53 | if scanNumber < 5 { 54 | // Progressively reduce blur as we load more scans. 55 | let radius = max(2, 14 - scanNumber * 4) 56 | let filter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius" : radius]) 57 | return container.map { 58 | ImageProcessors.CoreImageFilter.apply(filter: filter, to: $0) 59 | } 60 | } 61 | 62 | // Scans 5+ are already good enough not to blur them. 63 | return container 64 | #endif 65 | } 66 | 67 | let identifier: String = "_ProgressiveBlurImageProcessor" 68 | 69 | var hashableIdentifier: AnyHashable { 70 | return self 71 | } 72 | } 73 | 74 | extension ImageContainer { 75 | /// Modifies the wrapped image and keeps all of the context. 76 | func map(_ closure: (PlatformImage) -> PlatformImage?) -> ImageContainer? { 77 | guard let image = closure(self.image) else { 78 | return nil 79 | } 80 | return ImageContainer(image: image, isPreview: isPreview, data: data, userInfo: userInfo) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ImagePipelineSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | 8 | protocol ImagePipelineSettingsViewControllerDelegate: AnyObject { 9 | func imagePipelineSettingsViewController(_ vc: ImagePipelineSettingsViewController, didFinishWithConfiguration configuration: ImagePipeline.Configuration) 10 | } 11 | 12 | final class ImagePipelineSettingsViewController: UITableViewController { 13 | var configuration: ImagePipeline.Configuration! { 14 | didSet { 15 | guard isViewLoaded else { return } 16 | reload() 17 | } 18 | } 19 | 20 | weak var delegate: ImagePipelineSettingsViewControllerDelegate? 21 | 22 | @IBOutlet weak var optionDecompressionEnabledSwitch: UISwitch! 23 | @IBOutlet weak var optionDeduplicationEnabledSwitch: UISwitch! 24 | @IBOutlet weak var optionResumableDataEnabledSwitch: UISwitch! 25 | @IBOutlet weak var optionRateLimiterEnabledSwitch: UISwitch! 26 | @IBOutlet weak var optionProgressiveDecodingEnabledSwitch: UISwitch! 27 | 28 | @IBOutlet weak var memoryCacheEnabledSwitch: UISwitch! 29 | @IBOutlet weak var memoryCacheButtonClear: UIButton! 30 | @IBOutlet weak var memoryCacheTotalCost: UILabel! 31 | @IBOutlet weak var memoryCacheTotalCount: UILabel! 32 | 33 | @IBOutlet weak var urlCacheEnabledSwitch: UISwitch! 34 | @IBOutlet weak var urlCacheDetailsLabel: UILabel! 35 | @IBOutlet weak var urlCacheDataUsageLabel: UILabel! 36 | @IBOutlet weak var urlCacheMemoryUsageLabel: UILabel! 37 | @IBOutlet weak var urlCacheButtonClear: UIButton! 38 | 39 | @IBOutlet weak var dataCacheTitle: UILabel! 40 | @IBOutlet weak var dataCacheEnabledSwitch: UISwitch! 41 | @IBOutlet weak var dataCacheDataUsageCell: UITableViewCell! 42 | @IBOutlet weak var dataCacheTotalCountCell: UITableViewCell! 43 | @IBOutlet weak var dataCacheButtonClear: UIButton! 44 | 45 | @IBOutlet weak var queueDataLoadingValueLabel: UILabel! 46 | @IBOutlet weak var queueDataLoadingStepper: UIStepper! 47 | @IBOutlet weak var queueDataCachingValueLabel: UILabel! 48 | @IBOutlet weak var queueDataCachingStepper: UIStepper! 49 | @IBOutlet weak var queueDecodingValueLabel: UILabel! 50 | @IBOutlet weak var queueDecodingStepper: UIStepper! 51 | @IBOutlet weak var queueEncodingValueLabel: UILabel! 52 | @IBOutlet weak var queueEncodingStepper: UIStepper! 53 | @IBOutlet weak var queueProcessingValueLabel: UILabel! 54 | @IBOutlet weak var queueProcessingStepper: UIStepper! 55 | @IBOutlet weak var queueDecompressionValueLabel: UILabel! 56 | @IBOutlet weak var queueDecompressionStepper: UIStepper! 57 | 58 | static func show(from presentingViewController: UIViewController & ImagePipelineSettingsViewControllerDelegate, pipeline: ImagePipeline) { 59 | let navigationVC = UIStoryboard(name: "ImagePipelineSettingsViewController", bundle: nil).instantiateInitialViewController() as! UINavigationController 60 | let settingsVC = navigationVC.viewControllers[0] as! ImagePipelineSettingsViewController 61 | settingsVC.configuration = pipeline.configuration 62 | settingsVC.delegate = presentingViewController 63 | presentingViewController.present(navigationVC, animated: true, completion: nil) 64 | } 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | 69 | reload() 70 | reloadDataCache() 71 | reloadURLCache() 72 | } 73 | 74 | private func reload() { 75 | optionDecompressionEnabledSwitch.isOn = configuration.isDecompressionEnabled 76 | optionDeduplicationEnabledSwitch.isOn = configuration.isTaskCoalescingEnabled 77 | optionResumableDataEnabledSwitch.isOn = configuration.isResumableDataEnabled 78 | optionRateLimiterEnabledSwitch.isOn = configuration.isRateLimiterEnabled 79 | optionProgressiveDecodingEnabledSwitch.isOn = configuration.isProgressiveDecodingEnabled 80 | 81 | memoryCacheEnabledSwitch.isOn = configuration.imageCache != nil 82 | memoryCacheButtonClear.isEnabled = configuration.imageCache != nil 83 | if let imageCache = configuration.imageCache as? ImageCache { 84 | let formatter = ByteCountFormatter() 85 | formatter.countStyle = .binary 86 | memoryCacheTotalCost.text = "\(formatter.string(fromByteCount: Int64(imageCache.totalCost))) / \(formatter.string(fromByteCount: Int64(imageCache.costLimit)))" 87 | memoryCacheTotalCount.text = "\(imageCache.totalCount) / \(imageCache.countLimit == Int.max ? "Unlimited" : "\(imageCache.countLimit)")" 88 | } else { 89 | memoryCacheTotalCost.text = "Disabled" 90 | memoryCacheTotalCount.text = "Disabled" 91 | } 92 | 93 | urlCacheDetailsLabel.text = "Native URL HTTP cache used by URLSession" 94 | if let dataLoader = configuration.dataLoader as? DataLoader { 95 | let urlCache = dataLoader.session.configuration.urlCache 96 | urlCacheEnabledSwitch.isOn = urlCache != nil 97 | urlCacheButtonClear.isEnabled = urlCache != nil 98 | } else if let dataLoader = configuration.dataLoader as? AlamofireDataLoader { 99 | let urlCache = dataLoader.session.session.configuration.urlCache 100 | urlCacheEnabledSwitch.isOn = urlCache != nil 101 | urlCacheEnabledSwitch.isEnabled = false // Not supported 102 | urlCacheButtonClear.isEnabled = urlCache != nil 103 | urlCacheDetailsLabel.text = "Alamofire is used, some settings are disabled" 104 | } else { 105 | urlCacheEnabledSwitch.isEnabled = false 106 | urlCacheButtonClear.isEnabled = false 107 | urlCacheDataUsageLabel.text = "Unknown" 108 | urlCacheMemoryUsageLabel.text = "Unknown" 109 | urlCacheDetailsLabel.text = "Settings disabled – unknown data loader is used" 110 | } 111 | 112 | dataCacheTitle.text = "Data Cache" 113 | dataCacheEnabledSwitch.isOn = configuration.dataCache != nil 114 | dataCacheButtonClear.isEnabled = configuration.dataCache != nil 115 | 116 | if let _ = configuration.dataCache as? DataCache { 117 | // Do nothing 118 | } else if configuration.dataCache != nil { 119 | dataCacheTitle.text = "Data Cache (Custom)" 120 | dataCacheEnabledSwitch.isEnabled = false 121 | } else { 122 | // Do nothing 123 | } 124 | 125 | queueDataLoadingValueLabel.text = "\(configuration.dataLoadingQueue.maxConcurrentOperationCount)" 126 | queueDataLoadingStepper.value = Double(configuration.dataLoadingQueue.maxConcurrentOperationCount) 127 | queueDecodingValueLabel.text = "\(configuration.imageDecodingQueue.maxConcurrentOperationCount)" 128 | queueDecodingStepper.value = Double(configuration.imageDecodingQueue.maxConcurrentOperationCount) 129 | queueEncodingValueLabel.text = "\(configuration.imageEncodingQueue.maxConcurrentOperationCount)" 130 | queueEncodingStepper.value = Double(configuration.imageEncodingQueue.maxConcurrentOperationCount) 131 | queueProcessingValueLabel.text = "\(configuration.imageProcessingQueue.maxConcurrentOperationCount)" 132 | queueProcessingStepper.value = Double(configuration.imageProcessingQueue.maxConcurrentOperationCount) 133 | queueDecompressionValueLabel.text = "\(configuration.imageDecompressingQueue.maxConcurrentOperationCount)" 134 | queueDecompressionStepper.value = Double(configuration.imageDecompressingQueue.maxConcurrentOperationCount) 135 | } 136 | 137 | func reloadURLCache() { 138 | func display(urlCache: URLCache?) { 139 | if let urlCache = urlCache { 140 | let formatter = ByteCountFormatter() 141 | formatter.countStyle = .binary 142 | urlCacheDataUsageLabel.text = "\(formatter.string(fromByteCount: Int64(urlCache.currentDiskUsage))) / \(formatter.string(fromByteCount: Int64(urlCache.diskCapacity)))" 143 | urlCacheMemoryUsageLabel.text = "\(formatter.string(fromByteCount: Int64(urlCache.currentMemoryUsage))) / \(formatter.string(fromByteCount: Int64(urlCache.memoryCapacity)))" 144 | } else { 145 | urlCacheDataUsageLabel.text = "Disabled" 146 | urlCacheMemoryUsageLabel.text = "Disabled" 147 | } 148 | } 149 | 150 | if let dataLoader = configuration.dataLoader as? DataLoader { 151 | display(urlCache: dataLoader.session.configuration.urlCache) 152 | } else if let dataLoader = configuration.dataLoader as? AlamofireDataLoader { 153 | display(urlCache: dataLoader.session.session.configuration.urlCache) 154 | } else { 155 | urlCacheDataUsageLabel.text = "Unknown" 156 | urlCacheMemoryUsageLabel.text = "Unknown" 157 | } 158 | } 159 | 160 | func reloadDataCache() { 161 | if let dataCache = configuration.dataCache as? DataCache { 162 | let formatter = ByteCountFormatter() 163 | formatter.countStyle = .binary 164 | dataCacheDataUsageCell.detailTextLabel?.text = "\(formatter.string(fromByteCount: Int64(dataCache.totalSize))) / \(formatter.string(fromByteCount: Int64(dataCache.sizeLimit)))" 165 | dataCacheTotalCountCell.detailTextLabel?.text = "\(dataCache.totalCount)" 166 | } else if configuration.dataCache != nil { 167 | dataCacheDataUsageCell.detailTextLabel?.text = "Unknown" 168 | dataCacheTotalCountCell.detailTextLabel?.text = "Unknown" 169 | } else { 170 | dataCacheDataUsageCell.detailTextLabel?.text = "Disabled" 171 | dataCacheTotalCountCell.detailTextLabel?.text = "Disabled" 172 | } 173 | } 174 | 175 | // MARK: - Navigation 176 | 177 | @IBAction func buttonCancelTapped(_ sender: Any) { 178 | presentingViewController?.dismiss(animated: true, completion: nil) 179 | } 180 | 181 | @IBAction func buttonSaveTapped(_ sender: Any) { 182 | delegate?.imagePipelineSettingsViewController(self, didFinishWithConfiguration: configuration) 183 | } 184 | 185 | // MARK: - Actions 186 | 187 | @IBAction func switchDecompressionEnabledValueChanged(_ sender: UISwitch) { 188 | configuration.isDecompressionEnabled = sender.isOn 189 | } 190 | 191 | @IBAction func switchDeduplicationEnabledValueChanged(_ sender: UISwitch) { 192 | configuration.isTaskCoalescingEnabled = sender.isOn 193 | } 194 | 195 | @IBAction func switchResumableDataEnabledTapped(_ sender: UISwitch) { 196 | configuration.isResumableDataEnabled = sender.isOn 197 | } 198 | 199 | @IBAction func switchRateLimiterValueChanged(_ sender: UISwitch) { 200 | configuration.isRateLimiterEnabled = sender.isOn 201 | } 202 | 203 | @IBAction func switchProgressiveDecodingValueChanged(_ sender: UISwitch) { 204 | configuration.isProgressiveDecodingEnabled = sender.isOn 205 | } 206 | 207 | @IBAction func switchMemoryCachedEnabledValueChanged(_ sender: UISwitch) { 208 | configuration.imageCache = sender.isOn ? ImageCache.shared : nil 209 | } 210 | 211 | @IBAction func buttonClearMemoryCacheTapped(_ sender: Any) { 212 | ImageCache.shared.removeAll() 213 | reload() 214 | } 215 | 216 | // MARK: - Actions (URLCache) 217 | 218 | @IBAction func urlCacheSwitchEnabledValueChanged(_ sender: UISwitch) { 219 | let configuration = DataLoader.defaultConfiguration 220 | configuration.urlCache = sender.isOn ? DataLoader.sharedUrlCache : nil 221 | 222 | self.configuration.dataLoader = DataLoader(configuration: configuration) 223 | reloadURLCache() 224 | } 225 | 226 | @IBAction func buttonClearURLCacheTapped(_ sender: Any) { 227 | if let dataLoader = configuration.dataLoader as? DataLoader { 228 | dataLoader.session.configuration.urlCache?.removeAllCachedResponses() 229 | } else if let dataLoader = configuration.dataLoader as? AlamofireDataLoader { 230 | dataLoader.session.session.configuration.urlCache?.removeAllCachedResponses() 231 | } else { 232 | assertionFailure("Unsupported cache type") 233 | } 234 | 235 | reloadURLCache() 236 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 237 | self.reload() 238 | self.reloadURLCache() 239 | } 240 | } 241 | 242 | // MARK: - Actions (Data Cache) 243 | 244 | @IBAction func dataCacheSwitchValueChanged(_ sender: UISwitch) { 245 | configuration.dataCache = sender.isOn ? try? DataCache(name: "com.github.kean.Nuke.DataCache") : nil 246 | reloadDataCache() 247 | } 248 | 249 | @IBAction func dataCacheButtonClearTapped(_ sender: Any) { 250 | if let dataCache = configuration.dataCache as? DataCache { 251 | dataCache.removeAll() 252 | dataCache.flush() 253 | } 254 | reloadDataCache() 255 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 256 | self.reloadDataCache() 257 | } 258 | } 259 | 260 | // MARK: - Actions (Queues) 261 | 262 | @IBAction func stepperDataLoadingQueueValueChanged(_ sender: UIStepper) { 263 | configuration.dataLoadingQueue.maxConcurrentOperationCount = Int(sender.value) 264 | reload() 265 | } 266 | 267 | @IBAction func stepperDecodingQueueValueChanged(_ sender: UIStepper) { 268 | configuration.imageDecodingQueue.maxConcurrentOperationCount = Int(sender.value) 269 | reload() 270 | } 271 | 272 | @IBAction func stepperEncodingQueueValueChanged(_ sender: UIStepper) { 273 | configuration.imageEncodingQueue.maxConcurrentOperationCount = Int(sender.value) 274 | reload() 275 | } 276 | 277 | @IBAction func stepperProcessingQueueValueChanged(_ sender: UIStepper) { 278 | configuration.imageProcessingQueue.maxConcurrentOperationCount = Int(sender.value) 279 | reload() 280 | } 281 | 282 | @IBAction func stepperDecompressionQueueValueChanged(_ sender: UIStepper) { 283 | configuration.imageDecompressingQueue.maxConcurrentOperationCount = Int(sender.value) 284 | reload() 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Sources/ImageProcessingDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import NukeExtensions 8 | 9 | final class ImageProcessingDemoViewController: UIViewController, ImagePipelineSettingsViewControllerDelegate { 10 | private var pipeline = ImagePipeline.shared 11 | 12 | private let views: [[ImageProcessingView]] = [ 13 | [ImageProcessingView(), ImageProcessingView()], 14 | [ImageProcessingView(), ImageProcessingView()], 15 | [ImageProcessingView(), ImageProcessingView()] 16 | ] 17 | 18 | private let refreshControl = UIRefreshControl() 19 | private let scrollView = UIScrollView() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | view.backgroundColor = .systemBackground 25 | 26 | let hStacks: [UIStackView] = views.map { 27 | let stack = UIStackView(arrangedSubviews: $0) 28 | stack.axis = .horizontal 29 | stack.distribution = .fillEqually 30 | stack.spacing = 16 31 | return stack 32 | } 33 | 34 | let vStack = UIStackView(arrangedSubviews: hStacks) 35 | vStack.axis = .vertical 36 | vStack.spacing = 32 37 | vStack.isLayoutMarginsRelativeArrangement = true 38 | vStack.layoutMargins = UIEdgeInsets(top: 32, left: 16, bottom: 32, right: 16) 39 | 40 | let scrollView = UIScrollView() 41 | scrollView.addSubview(vStack) 42 | vStack.pinToSuperview() 43 | 44 | view.addSubview(scrollView) 45 | scrollView.pinToSuperview() 46 | 47 | vStack.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true 48 | 49 | scrollView.refreshControl = refreshControl 50 | scrollView.refreshControl?.addTarget(self, action: #selector(refreshControlValueChanged), for: .valueChanged) 51 | scrollView.alwaysBounceVertical = true 52 | 53 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Configuration", style: .plain, target: self, action: #selector(buttonShowSettingsTapped)) 54 | 55 | // Displaying images in a grid the way we want is a bit tricky on iOS so 56 | // we use aspect ratio is be sure it works correctly in all scenarios 57 | views[0][0].aspectRatio = 3/2 58 | views[0][1].aspectRatio = 3/2 59 | views[1][0].aspectRatio = 3/2 60 | views[1][1].aspectRatio = 3/2 61 | views[2][0].aspectRatio = 1 62 | views[2][1].aspectRatio = 1 63 | 64 | loadImages() 65 | } 66 | 67 | func loadImages() { 68 | loadImage(view: views[0][0], title: "Original", processors: []) 69 | 70 | let screenWidth = UIScreen.main.bounds.size.width / 3 71 | let targetSize = CGSize(width: screenWidth, height: (screenWidth * 2 / 3)) 72 | loadImage(view: views[0][1], title: "Resize", processors: [ 73 | ImageProcessors.Resize(size: targetSize) 74 | ]) 75 | 76 | loadImage(view: views[1][0], title: "Rounded Corners", processors: [ 77 | ImageProcessors.Resize(size: targetSize), 78 | ImageProcessors.RoundedCorners(radius: 8) 79 | ]) 80 | 81 | loadImage(view: views[1][1], title: "Monochrome", processors: [ 82 | ImageProcessors.Resize(size: targetSize), 83 | ImageProcessors.RoundedCorners(radius: 8), 84 | ImageProcessors.CoreImageFilter(name: "CIColorMonochrome", 85 | parameters: ["inputIntensity": 1, 86 | "inputColor": CIColor(color: .white)], 87 | identifier: "nuke.demo.monochrome") 88 | ]) 89 | 90 | loadImage(view: views[2][0], title: "Circle", processors: [ 91 | ImageProcessors.Resize(size: targetSize), 92 | ImageProcessors.Circle() 93 | ]) 94 | 95 | loadImage(view: views[2][1], title: "Blur", processors: [ 96 | ImageProcessors.Resize(size: targetSize), 97 | ImageProcessors.Circle(), 98 | ImageProcessors.GaussianBlur(radius: 3) 99 | ]) 100 | } 101 | 102 | private func loadImage(view: ImageProcessingView, title: String, processors: [ImageProcessing]) { 103 | let request = ImageRequest( 104 | url: URL(string: "https://user-images.githubusercontent.com/1567433/59150453-178bbb80-8a24-11e9-94ca-fd8dff6e2a9a.jpeg")!, 105 | processors: processors 106 | ) 107 | 108 | view.titleLabel.text = title 109 | 110 | var options = ImageLoadingOptions(transition: .fadeIn(duration: 0.5)) 111 | options.pipeline = pipeline 112 | 113 | NukeExtensions.loadImage(with: request, options: options, into: view.imageView) 114 | } 115 | 116 | // MARK: - Actions 117 | 118 | @objc private func refreshControlValueChanged() { 119 | loadImages() 120 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { 121 | self.refreshControl.endRefreshing() 122 | } 123 | } 124 | 125 | @objc private func buttonShowSettingsTapped() { 126 | ImagePipelineSettingsViewController.show(from: self, pipeline: pipeline) 127 | } 128 | 129 | // MARK: - ImagePipelineSettingsViewControllerDelegate 130 | 131 | func imagePipelineSettingsViewController(_ vc: ImagePipelineSettingsViewController, didFinishWithConfiguration configuration: ImagePipeline.Configuration) { 132 | self.pipeline = ImagePipeline(configuration: configuration) 133 | vc.dismiss(animated: true) {} 134 | } 135 | } 136 | 137 | // MARK: - ImageProcessingView 138 | 139 | private class ImageProcessingView: UIView { 140 | let titleLabel = UILabel() 141 | let imageView = UIImageView() 142 | 143 | override init(frame: CGRect) { 144 | super.init(frame: frame) 145 | 146 | titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) 147 | 148 | imageView.contentMode = .scaleAspectFit 149 | imageView.clipsToBounds = true 150 | 151 | let container = UIView() 152 | container.addSubview(imageView) 153 | imageView.translatesAutoresizingMaskIntoConstraints = false 154 | NSLayoutConstraint.activate([ 155 | imageView.leftAnchor.constraint(equalTo: container.leftAnchor), 156 | imageView.topAnchor.constraint(equalTo: container.topAnchor), 157 | imageView.rightAnchor.constraint(lessThanOrEqualTo: container.rightAnchor), 158 | imageView.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor) 159 | ]) 160 | 161 | let stack = UIStackView(arrangedSubviews: [titleLabel, container]) 162 | stack.axis = .vertical 163 | stack.spacing = 8 164 | 165 | addSubview(stack) 166 | stack.pinToSuperview() 167 | 168 | container.translatesAutoresizingMaskIntoConstraints = false 169 | NSLayoutConstraint.activate([ 170 | container.heightAnchor.constraint(equalToConstant: 100) 171 | ]) 172 | } 173 | 174 | var aspectRatio: CGFloat = 1 { 175 | didSet { 176 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: aspectRatio).isActive = true 177 | } 178 | } 179 | 180 | required init?(coder: NSCoder) { 181 | fatalError("init(coder:) has not been implemented") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Sources/LazyImageDemo.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import SwiftUI 6 | import Nuke 7 | import NukeUI 8 | 9 | @MainActor 10 | struct LazyImageDemoView: View { 11 | private let items = allItems 12 | @State private var listId = UUID() 13 | 14 | private let pipeline = ImagePipeline { 15 | $0.dataLoader = { 16 | let config = URLSessionConfiguration.default 17 | config.urlCache = nil 18 | return DataLoader(configuration: config) 19 | }() 20 | } 21 | 22 | var body: some View { 23 | List(items) { item in 24 | let view = VStack(spacing: 16) { 25 | Text(item.title) 26 | .font(.headline) 27 | .padding(.top, 32) 28 | makeImage(url: item.url) 29 | }.listRowInsets(EdgeInsets(.zero)) 30 | if #available(iOS 15, *) { 31 | view.listRowSeparator(.hidden) 32 | } else { 33 | view 34 | } 35 | } 36 | .id(listId) 37 | .navigationBarItems(trailing: Button(action: { 38 | ImagePipeline.shared.cache.removeAll() 39 | self.listId = UUID() 40 | }, label: { 41 | Image(systemName: "arrow.clockwise") 42 | })) 43 | .listStyle(.plain) 44 | } 45 | 46 | // This is where the image view is created. 47 | func makeImage(url: URL) -> some View { 48 | LazyImage(url: url) { state in 49 | if let container = state.imageContainer, container.type == .gif, let data = container.data { 50 | GIFImage(data: data) 51 | } else if let image = state.image { 52 | image 53 | .resizable() 54 | .aspectRatio(contentMode: .fill) 55 | } else { 56 | Color.gray.opacity(0.2) // Placeholder 57 | } 58 | } 59 | .pipeline(pipeline) 60 | .frame(height: 320) 61 | } 62 | } 63 | 64 | private struct Item: Identifiable { 65 | var id: String { title } 66 | let title: String 67 | let url: URL 68 | } 69 | 70 | private let allItems = [ 71 | Item(title: "Baseline JPEG", url: URL(string: "https://user-images.githubusercontent.com/1567433/120257591-80e2e580-c25e-11eb-8032-54f3a966aedb.jpeg")!), 72 | Item(title: "Progressive JPEG", url: URL(string: "https://user-images.githubusercontent.com/1567433/120257587-7fb1b880-c25e-11eb-93d1-7e7df2b9f5ca.jpeg")!), 73 | Item(title: "Animated GIF", url: URL(string: "https://cloud.githubusercontent.com/assets/1567433/6505557/77ff05ac-c2e7-11e4-9a09-ce5b7995cad0.gif")!), 74 | Item(title: "WebP", url: URL(string: "https://kean.github.io/images/misc/4.webp")!) 75 | ] 76 | -------------------------------------------------------------------------------- /Sources/MenuViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import SwiftUI 8 | import Pulse 9 | import PulseUI 10 | 11 | final class MenuViewController: UITableViewController { 12 | private var sections = [MenuSection]() 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | navigationController?.navigationBar.prefersLargeTitles = true 18 | navigationItem.largeTitleDisplayMode = .automatic 19 | 20 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Network Logs", style: .plain, target: self, action: #selector(showNetworkLogs)) 21 | 22 | sections = [generalSection, integrationSection, advancedSection] 23 | 24 | (ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = URLSessionProxyDelegate() 25 | } 26 | 27 | private var generalSection: MenuSection { 28 | var items = [ 29 | MenuItem( 30 | title: "Image Pipeline", 31 | subtitle: "The default pipeline, configurable at runtime", 32 | action: { [weak self] in self?.push(BasicDemoViewController(), $0) } 33 | ), 34 | MenuItem( 35 | title: "Image Processing", 36 | subtitle: "Showcases some of the built-in image processors", 37 | action: { [weak self] in self?.push(ImageProcessingDemoViewController(), $0) } 38 | ), 39 | MenuItem( 40 | title: "Disk Cache", 41 | subtitle: "Aggressive disk caching enabled", 42 | action: { [weak self] in self?.push(DataCachingDemoViewController(), $0) } 43 | ) 44 | ] 45 | items.append(MenuItem( 46 | title: "LazyImage", 47 | subtitle: "NukeUI package demo (SwiftUI)", 48 | action: { [weak self] in self?.push(UIHostingController(rootView: LazyImageDemoView()), $0) } 49 | )) 50 | return MenuSection(title: "General", items: items) 51 | } 52 | 53 | private var integrationSection: MenuSection { 54 | MenuSection(title: "Integrations", items: [ 55 | MenuItem( 56 | title: "Alamofire", 57 | subtitle: "Custom networking stack", 58 | action: { [weak self] in self?.push(AlamofireIntegrationDemoViewController(), $0) } 59 | ), 60 | MenuItem( 61 | title: "Gifu", 62 | subtitle: "Display animated GIFs", 63 | action: { [weak self] in self?.push(GifuDemoViewController(), $0) } 64 | ), 65 | MenuItem( 66 | title: "SwiftSVG", 67 | subtitle: "Render vector images", 68 | action: { [weak self] in self?.push(SwiftSVGDemoViewController(), $0) } 69 | ) 70 | ]) 71 | } 72 | 73 | private var advancedSection: MenuSection { 74 | var items = [ 75 | MenuItem( 76 | title: "Prefetch (UIKit)", 77 | subtitle: "UICollectionView Prefetching", 78 | action: { [weak self] in self?.push(PrefetchingDemoViewController(), $0) } 79 | ) 80 | ] 81 | items.append(MenuItem( 82 | title: "Prefetch (SwiftUI)", 83 | subtitle: "LazyVGrid and FetchImage", 84 | action: { [weak self] in self?.push(UIHostingController(rootView: PrefetchDemoView()), $0) } 85 | )) 86 | items += [ 87 | MenuItem( 88 | title: "Progressive JPEG", 89 | subtitle: "Progressive vs baseline JPEG", 90 | action: { [weak self] in self?.push(ProgressiveDecodingDemoViewController(), $0) } 91 | ), 92 | MenuItem( 93 | title: "Rate Limiter", 94 | subtitle: "Infinite scroll, highlights rate limiter performance", 95 | action: { [weak self] in self?.push(RateLimiterDemoViewController(), $0) } 96 | ), 97 | MenuItem( 98 | title: "MP4 (Experimental)", 99 | subtitle: "Replaces GIFs with MP4", 100 | action: { [weak self] in self?.push(AnimatedImageUsingVideoViewController(), $0) 101 | }) 102 | ] 103 | 104 | return MenuSection(title: "Advanced", items: items) 105 | } 106 | 107 | private func push(_ controller: UIViewController, _ item: MenuItem) { 108 | controller.title = item.title 109 | navigationController?.pushViewController(controller, animated: true) 110 | } 111 | 112 | @objc private func showNetworkLogs() { 113 | present(MainViewController(), animated: true) 114 | } 115 | 116 | // MARK: Table View 117 | 118 | override func numberOfSections(in tableView: UITableView) -> Int { 119 | sections.count 120 | } 121 | 122 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 123 | sections[section].items.count 124 | } 125 | 126 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 127 | sections[section].title 128 | } 129 | 130 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 131 | let cell = tableView.dequeueReusableCell(withIdentifier: "MenuItemCell", for: indexPath) 132 | let item = sections[indexPath.section].items[indexPath.row] 133 | cell.textLabel?.text = item.title 134 | cell.detailTextLabel?.text = item.subtitle 135 | return cell 136 | } 137 | 138 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 139 | let item = sections[indexPath.section].items[indexPath.row] 140 | item.action?(item) 141 | } 142 | } 143 | 144 | // MARK - MenuItem 145 | 146 | private struct MenuItem { 147 | typealias Action = ((MenuItem) -> Void) 148 | 149 | var title: String? 150 | var subtitle: String? 151 | var action: Action? 152 | 153 | init(title: String?, subtitle: String? = nil, action: Action?) { 154 | self.title = title 155 | self.subtitle = subtitle 156 | self.action = action 157 | } 158 | } 159 | 160 | private struct MenuSection { 161 | var title: String 162 | var items: [MenuItem] 163 | 164 | init(title: String, items: [MenuItem]) { 165 | self.title = title 166 | self.items = items 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/PrefetchSwiftUIDemo.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import Foundation 6 | import SwiftUI 7 | import Nuke 8 | import NukeUI 9 | 10 | // MARK: - View 11 | 12 | struct PrefetchDemoView: View { 13 | @StateObject var model = PrefetchDemoViewModel() 14 | 15 | var body: some View { 16 | GeometryReader { geometry in 17 | ScrollView { 18 | let side = geometry.size.width / 4 19 | let item = GridItem(.fixed(side), spacing: 2) 20 | LazyVGrid(columns: Array(repeating: item, count: 4), spacing: 2) { 21 | ForEach(0.. Range { 68 | urls.indices 69 | } 70 | 71 | func prefetcher(_ prefetcher: ScrollViewPrefetcher, prefetchItemsAt indices: [Int]) { 72 | imagePrefetcher.startPrefetching(with: indices.map { urls[$0] }) 73 | } 74 | 75 | func prefetcher(_ prefetcher: ScrollViewPrefetcher, cancelPrefechingForItemAt indices: [Int]) { 76 | imagePrefetcher.stopPrefetching(with: indices.map { urls[$0] }) 77 | } 78 | } 79 | 80 | // MARK: - ScrollViewPrefetcher 81 | 82 | protocol ScrollViewPrefetcherDelegate: AnyObject { 83 | /// Returns all valid indices for the collection. 84 | func getAllIndicesForPrefetcher(_ prefetcher: ScrollViewPrefetcher) -> Range 85 | func prefetcher(_ prefetcher: ScrollViewPrefetcher, prefetchItemsAt indices: [Int]) 86 | func prefetcher(_ prefetcher: ScrollViewPrefetcher, cancelPrefechingForItemAt indices: [Int]) 87 | } 88 | 89 | final class ScrollViewPrefetcher { 90 | private let prefetchWindowSize: Int 91 | 92 | weak var delegate: ScrollViewPrefetcherDelegate? 93 | 94 | private var visibleIndices: Set = [] 95 | private var flushedVisibleIndices: Set = [] 96 | private var isRefreshScheduled = false 97 | private var prefetchWindow = 0..<0 98 | 99 | init(prefetchWindowSize: Int = 12) { 100 | self.prefetchWindowSize = prefetchWindowSize 101 | } 102 | 103 | func onAppear(_ index: Int) { 104 | visibleIndices.insert(index) 105 | scheduleRefreshIfNeeded() 106 | } 107 | 108 | func onDisappear(_ index: Int) { 109 | visibleIndices.remove(index) 110 | scheduleRefreshIfNeeded() 111 | } 112 | 113 | /// SwiftUI sometimes calls onAppear in unexpected order, buffer takes care of it. 114 | private func scheduleRefreshIfNeeded() { 115 | guard !isRefreshScheduled else { return } 116 | isRefreshScheduled = true 117 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in 118 | self?.updatePrefetchWindow() 119 | } 120 | } 121 | 122 | private func updatePrefetchWindow() { 123 | isRefreshScheduled = false 124 | guard !visibleIndices.isEmpty else { return } // Should never happen 125 | 126 | if flushedVisibleIndices.isEmpty { 127 | // Showing screen for the first time 128 | let lowerBound = visibleIndices.max()! + 1 129 | updatePrefetchWindow(lowerBound..<(lowerBound + prefetchWindowSize)) 130 | } else { 131 | // Need to figure out in which direction we the user is scrolling 132 | let isScrollingDown = visibleIndices.max()! > flushedVisibleIndices.max()! 133 | if isScrollingDown || visibleIndices.contains(0) { 134 | let lowerBound = visibleIndices.max()! + 1 135 | updatePrefetchWindow(lowerBound..<(lowerBound + prefetchWindowSize)) 136 | } else { 137 | let upperBound = visibleIndices.min()! - 1 138 | updatePrefetchWindow((upperBound - prefetchWindowSize)..) { 146 | let oldPrefetchIndices = Set(prefetchWindow) 147 | let newPrefetchIndices = Set(newPrefetchWindow) 148 | 149 | prefetchWindow = newPrefetchWindow 150 | 151 | let allIndices = Set(delegate?.getAllIndicesForPrefetcher(self) ?? 0..<0) 152 | 153 | let removedIndicides = oldPrefetchIndices 154 | .subtracting(newPrefetchIndices) 155 | .intersection(allIndices) // Only call for visible items 156 | .sorted() 157 | let addedIndices = newPrefetchIndices 158 | .subtracting(oldPrefetchIndices) 159 | .intersection(allIndices) // Only call for visible items 160 | .sorted() 161 | 162 | delegate?.prefetcher(self, prefetchItemsAt: addedIndices) 163 | delegate?.prefetcher(self, cancelPrefechingForItemAt: removedIndicides) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Sources/PrefetchingDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | 8 | private let cellReuseID = "reuseID" 9 | private var loggingEnabled = true 10 | 11 | final class PrefetchingDemoViewController: BasicDemoViewController, UICollectionViewDataSourcePrefetching { 12 | let prefetcher = ImagePrefetcher() 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | collectionView?.isPrefetchingEnabled = true 18 | collectionView?.prefetchDataSource = self 19 | } 20 | 21 | override func viewWillAppear(_ animated: Bool) { 22 | super.viewWillAppear(animated) 23 | 24 | prefetcher.isPaused = false 25 | } 26 | 27 | override func viewWillDisappear(_ animated: Bool) { 28 | super.viewWillDisappear(animated) 29 | 30 | prefetcher.isPaused = true 31 | } 32 | 33 | // MARK: UICollectionViewDataSourcePrefetching 34 | 35 | func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { 36 | let urls = indexPaths.map { photos[$0.row] } 37 | prefetcher.startPrefetching(with: urls) 38 | if loggingEnabled { 39 | print("prefetchItemsAt: \(stringForIndexPaths(indexPaths))") 40 | } 41 | } 42 | 43 | func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { 44 | let urls = indexPaths.map { photos[$0.row] } 45 | prefetcher.stopPrefetching(with: urls) 46 | if loggingEnabled { 47 | print("cancelPrefetchingForItemsAt: \(stringForIndexPaths(indexPaths))") 48 | } 49 | } 50 | } 51 | 52 | private func stringForIndexPaths(_ indexPaths: [IndexPath]) -> String { 53 | guard indexPaths.count > 0 else { 54 | return "[]" 55 | } 56 | let items = indexPaths 57 | .map { return "\(($0 as NSIndexPath).item)" } 58 | .joined(separator: " ") 59 | return "[\(items)]" 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ProgressiveDecodingDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import NukeExtensions 8 | 9 | final class ProgressiveDecodingDemoViewController: UIViewController { 10 | private let urls = [ 11 | URL(string: "mock-loader://progressive")!, 12 | URL(string: "mock-loader://baseline")! 13 | ] 14 | 15 | private let pipeline = ImagePipeline { 16 | $0.dataLoader = _MockDataLoader() 17 | $0.imageCache = nil 18 | $0.isTaskCoalescingEnabled = false 19 | $0.isProgressiveDecodingEnabled = true 20 | } 21 | 22 | private let segmentedControl = UISegmentedControl(items: ["Progressive", "Baseline"]) 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | view.backgroundColor = UIColor.systemBackground 28 | 29 | segmentedControl.selectedSegmentIndex = 0 30 | segmentedControl.addTarget(self, action: #selector(_segmentedControlValueChanged(_:)), for: .valueChanged) 31 | 32 | self.navigationItem.titleView = segmentedControl 33 | 34 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(_refresh)) 35 | 36 | _start(with: urls[0]) 37 | } 38 | 39 | private func _start(with url: URL) { 40 | self.title = segmentedControl.selectedSegmentIndex == 0 ? "Progressive JPEG" : "Baseline JPEG" 41 | 42 | view.viewWithTag(12)?.removeFromSuperview() 43 | 44 | let container = ProgressiveImageView() 45 | container.tag = 12 46 | 47 | view.addSubview(container) 48 | 49 | container.translatesAutoresizingMaskIntoConstraints = false 50 | NSLayoutConstraint.activate( 51 | [container.topAnchor.constraint(equalTo: view.topAnchor), 52 | container.leftAnchor.constraint(equalTo: view.leftAnchor), 53 | container.rightAnchor.constraint(equalTo: view.rightAnchor)] 54 | ) 55 | 56 | let imageView = container.imageView 57 | 58 | var options = ImageLoadingOptions() 59 | // Use our custom pipeline with progressive decoding enabled and 60 | // _MockDataLoader which returns data on predifined intervals. 61 | options.pipeline = pipeline 62 | options.isProgressiveRenderingEnabled = segmentedControl.selectedSegmentIndex == 0 63 | options.transition = .fadeIn(duration: 0.25) 64 | 65 | loadImage( 66 | with: ImageRequest(url: url, processors: [_ProgressiveBlurImageProcessor()]), 67 | options: options, 68 | into: imageView, 69 | progress: { _, completed, total in 70 | container.updateProgress(completed: completed, total: total) 71 | } 72 | ) 73 | } 74 | 75 | @objc func _segmentedControlValueChanged(_ segmentedControl: UISegmentedControl) { 76 | _start(with: urls[segmentedControl.selectedSegmentIndex]) 77 | } 78 | 79 | @objc func _refresh() { 80 | DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { 81 | self._start(with: self.urls[self.segmentedControl.selectedSegmentIndex]) 82 | } 83 | } 84 | } 85 | 86 | private final class ProgressiveImageView: UIView { 87 | let imageView = UIImageView() 88 | let labelProgress = UILabel() 89 | 90 | override init(frame: CGRect) { 91 | super.init(frame: frame) 92 | 93 | _createUI() 94 | 95 | imageView.contentMode = .scaleAspectFill 96 | imageView.backgroundColor = UIColor.gray.withAlphaComponent(0.15) 97 | imageView.clipsToBounds = true 98 | 99 | labelProgress.text = "" 100 | } 101 | 102 | required init?(coder aDecoder: NSCoder) { 103 | fatalError("init(coder:) has not been implemented") 104 | } 105 | 106 | private func _createUI() { 107 | let labels = UIStackView(arrangedSubviews: [labelProgress]) 108 | labels.axis = .vertical 109 | labels.spacing = 15 110 | labels.isLayoutMarginsRelativeArrangement = true 111 | labels.layoutMargins = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) 112 | 113 | let stack = UIStackView(arrangedSubviews: [imageView, labels]) 114 | stack.axis = .vertical 115 | stack.spacing = 0 116 | stack.isLayoutMarginsRelativeArrangement = true 117 | stack.layoutMargins = UIEdgeInsets(top: 15, left: 0, bottom: 15, right: 0) 118 | 119 | addSubview(stack) 120 | stack.translatesAutoresizingMaskIntoConstraints = false 121 | stack.pinToSuperview() 122 | 123 | imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 4.0 / 3.0).isActive = true 124 | } 125 | 126 | func updateProgress(completed: Int64, total: Int64) { 127 | let text = NSMutableAttributedString(string: "") 128 | text.append(NSAttributedString( 129 | string: "Downloaded: ", 130 | attributes: [.font: UIFont.boldSystemFont(ofSize: 18)] 131 | )) 132 | let completed = ByteCountFormatter.string(fromByteCount: completed, countStyle: .binary) 133 | let total = ByteCountFormatter.string(fromByteCount: total, countStyle: .binary) 134 | text.append(NSAttributedString( 135 | string: "\(completed) / \(total)", 136 | attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: 17, weight: .regular)] 137 | )) 138 | labelProgress.attributedText = text 139 | } 140 | } 141 | 142 | private final class _MockDataLoader: DataLoading { 143 | private final class _MockTask: Cancellable { 144 | func cancel() { } 145 | } 146 | 147 | func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { 148 | let data = _data(for: request) 149 | let chunks = _createChunks(for: data, size: 10240) 150 | let response = URLResponse(url: request.url!, mimeType: "image/jpeg", expectedContentLength: data.count, textEncodingName: nil) 151 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in 152 | self?._sendData(chunks, didReceiveData: { didReceiveData($0, response) }, completion: completion) 153 | } 154 | return _MockTask() 155 | } 156 | 157 | func removeData(for request: URLRequest) { 158 | // Do nothing 159 | } 160 | 161 | private func _sendData(_ data: [Data], didReceiveData: @escaping (Data) -> Void, completion: @escaping (Error?) -> Void) { 162 | guard !data.isEmpty else { 163 | completion(nil) 164 | return 165 | } 166 | let (x, xs) = (data[0], Array(data.dropFirst())) 167 | didReceiveData(x) 168 | DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in 169 | self?._sendData(xs, didReceiveData: didReceiveData, completion: completion) 170 | } 171 | } 172 | 173 | private func _data(for request: URLRequest) -> Data { 174 | let url = Bundle.main.url(forResource: request.url!.lastPathComponent, withExtension: "jpeg")! 175 | return try! Data(contentsOf: url) 176 | } 177 | } 178 | 179 | private func _createChunks(for data: Data, size: Int) -> [Data] { 180 | var chunks = [Data]() 181 | let totalSize = data.count 182 | var offset = 0 183 | while offset < totalSize { 184 | let chunkSize = offset + size > totalSize ? totalSize - offset : size 185 | let chunk = data[offset..<(offset + chunkSize)] 186 | offset += chunkSize 187 | chunks.append(chunk) 188 | } 189 | return chunks 190 | } 191 | -------------------------------------------------------------------------------- /Sources/RateLimiterDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import NukeExtensions 8 | 9 | private let cellReuseID = "reuseID" 10 | 11 | final class RateLimiterDemoViewController: BasicDemoViewController { 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | itemsPerRow = 10 16 | 17 | pipeline = ImagePipeline { 18 | let urlSessionConf = URLSessionConfiguration.default 19 | urlSessionConf.urlCache = nil // disable disk cache 20 | $0.dataLoader = DataLoader(configuration: urlSessionConf) 21 | 22 | $0.imageCache = nil // disable memory cache 23 | 24 | $0.isTaskCoalescingEnabled = false // disable deduplication 25 | } 26 | 27 | for _ in 0..<10 { 28 | photos.append(contentsOf: photos) 29 | } 30 | } 31 | 32 | override func makeRequest(with url: URL, cellSize: CGSize) -> ImageRequest { 33 | return ImageRequest(url: url, processors: [ImageProcessors.Resize(size: cellSize)]) 34 | } 35 | 36 | override func makeImageLoadingOptions() -> ImageLoadingOptions { 37 | return ImageLoadingOptions() // no transition animations 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftSVGDemoViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). 4 | 5 | import UIKit 6 | import Nuke 7 | import SwiftSVG 8 | 9 | final class SwiftSVGDemoViewController: UIViewController { 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | view.backgroundColor = UIColor.systemBackground 14 | 15 | ImageDecoderRegistry.shared.register { context in 16 | // Replace this with whatever works for. There are no magic numbers 17 | // for SVG like are used for other binary formats, it's just XML. 18 | let isSVG = context.urlResponse?.url?.absoluteString.hasSuffix(".svg") ?? false 19 | return isSVG ? ImageDecoders.Empty() : nil 20 | } 21 | 22 | let url = URL(string: "https://upload.wikimedia.org/wikipedia/commons/9/9d/Swift_logo.svg")! 23 | 24 | // If you know that all images downloaded by the pipeline are going to be SVG, 25 | // you can just call `loadData(with:)` instead and not register an Empty decoder. 26 | ImagePipeline.shared.loadImage(with: url) { [weak self] result in 27 | guard let self = self, let data = try? result.get().container.data else { 28 | return 29 | } 30 | 31 | // You can render image using whatever size you want, vector! 32 | let targetBounds = CGRect(origin: .zero, size: CGSize(width: 300, height: 300)) 33 | let svgView = UIView(SVGData: data) { layer in 34 | layer.resizeToFit(targetBounds) 35 | } 36 | self.view.addSubview(svgView) 37 | 38 | svgView.bounds = targetBounds 39 | svgView.center = self.view.center 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------