├── .gitignore ├── .swift-version ├── .travis.yml ├── Example ├── ImageLoaderExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── ImageLoaderExample │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── BlockingMainThreadPerfomanceTestViewController.swift │ ├── CPUAndMemoryPeformanceTestViewController.swift │ ├── CollectionViewController.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ ├── SimpleViewController.swift │ ├── TableViewController.swift │ ├── Utils.swift │ ├── Watchdog.swift │ └── black.jpg ├── Gemfile ├── Gemfile.lock ├── ImageLoader.podspec ├── ImageLoader.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── ImageLoader.xcscheme ├── ImageLoader.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings ├── ImageLoader ├── Core │ ├── Disk.swift │ ├── HashStorage.swift │ ├── ImageLoader.swift │ ├── Loadable.swift │ ├── Loader.swift │ ├── Operative.swift │ ├── Option.swift │ └── URLLiteralConvertible.swift ├── Extension │ ├── CGImageSource+ImageLoader.swift │ ├── Data+ImageLoader.swift │ ├── UIImage+ImageLoader.swift │ └── UIImageView+ImageLoader.swift ├── ImageLoader.h └── Info.plist ├── ImageLoaderTests ├── DiskTests.swift ├── ImageLoaderTestCase.swift ├── ImageLoaderTests.swift ├── Info.plist ├── UIImageTests.swift ├── UIImageViewTests.swift ├── URLLiteralConvertibleTests.swift ├── black.png └── white.png ├── LICENSE ├── README.md └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 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 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | Carthage/Checkouts 42 | 43 | Carthage/Build 44 | 45 | # Ruby 46 | .bundle 47 | vendor/bundle/ 48 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.3beta 3 | 4 | before_install: 5 | - gem install xcpretty 6 | 7 | script: 8 | - rake test 9 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 650CA45C205F8D6B00553C6F /* ImageLoader.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 650CA45D205F8D6B00553C6F /* ImageLoader.framework */; }; 11 | 6578B7341A5BE1FE008D85D6 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65D6FBA81A03855700CF1DB7 /* LaunchScreen.xib */; }; 12 | 659D34AB1AD7A2B800FEFABC /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 659D34AA1AD7A2B800FEFABC /* CollectionViewController.swift */; }; 13 | 65AB21F71A5BE3C1007E6D40 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65D6FBA31A03855700CF1DB7 /* Main.storyboard */; }; 14 | 65D6FBA01A03855700CF1DB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D6FB9F1A03855700CF1DB7 /* AppDelegate.swift */; }; 15 | 65D6FBA71A03855700CF1DB7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65D6FBA61A03855700CF1DB7 /* Images.xcassets */; }; 16 | 65D6FBC41A03859500CF1DB7 /* SimpleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D6FBC11A03859500CF1DB7 /* SimpleViewController.swift */; }; 17 | 65D6FBC61A03859E00CF1DB7 /* black.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 65D6FBC51A03859E00CF1DB7 /* black.jpg */; }; 18 | A13D8A431C03498A00B59641 /* CPUAndMemoryPeformanceTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13D8A421C03498A00B59641 /* CPUAndMemoryPeformanceTestViewController.swift */; }; 19 | A13D8A451C0349F600B59641 /* BlockingMainThreadPerfomanceTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A13D8A441C0349F600B59641 /* BlockingMainThreadPerfomanceTestViewController.swift */; }; 20 | A168B1091BDBDF9900646115 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A168B1081BDBDF9900646115 /* TableViewController.swift */; }; 21 | A1B052591C03469500B67C0F /* Watchdog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B052581C03469500B67C0F /* Watchdog.swift */; }; 22 | A1EE65CC1A41F9DF00BEA147 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1EE65CB1A41F9DF00BEA147 /* Utils.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 650CA45D205F8D6B00553C6F /* ImageLoader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ImageLoader.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 659D34AA1AD7A2B800FEFABC /* CollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CollectionViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 28 | 65D6FB9A1A03855700CF1DB7 /* ImageLoaderExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ImageLoaderExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 65D6FB9E1A03855700CF1DB7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | 65D6FB9F1A03855700CF1DB7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 31 | 65D6FBA41A03855700CF1DB7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 32 | 65D6FBA61A03855700CF1DB7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 33 | 65D6FBA91A03855700CF1DB7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 34 | 65D6FBC11A03859500CF1DB7 /* SimpleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SimpleViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 35 | 65D6FBC51A03859E00CF1DB7 /* black.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = black.jpg; sourceTree = ""; }; 36 | A13D8A421C03498A00B59641 /* CPUAndMemoryPeformanceTestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CPUAndMemoryPeformanceTestViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 37 | A13D8A441C0349F600B59641 /* BlockingMainThreadPerfomanceTestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BlockingMainThreadPerfomanceTestViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 38 | A168B1081BDBDF9900646115 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TableViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 39 | A1B052581C03469500B67C0F /* Watchdog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Watchdog.swift; sourceTree = ""; }; 40 | A1EE65CB1A41F9DF00BEA147 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Utils.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 65D6FB971A03855700CF1DB7 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | 650CA45C205F8D6B00553C6F /* ImageLoader.framework in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 650CA45B205F8D6B00553C6F /* Frameworks */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 650CA45D205F8D6B00553C6F /* ImageLoader.framework */, 59 | ); 60 | name = Frameworks; 61 | sourceTree = ""; 62 | }; 63 | 65D6FB911A03855700CF1DB7 = { 64 | isa = PBXGroup; 65 | children = ( 66 | 65D6FB9C1A03855700CF1DB7 /* ImageLoaderExample */, 67 | 65D6FB9B1A03855700CF1DB7 /* Products */, 68 | 650CA45B205F8D6B00553C6F /* Frameworks */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | 65D6FB9B1A03855700CF1DB7 /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 65D6FB9A1A03855700CF1DB7 /* ImageLoaderExample.app */, 76 | ); 77 | name = Products; 78 | sourceTree = ""; 79 | }; 80 | 65D6FB9C1A03855700CF1DB7 /* ImageLoaderExample */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 65D6FB9F1A03855700CF1DB7 /* AppDelegate.swift */, 84 | 65D6FBA31A03855700CF1DB7 /* Main.storyboard */, 85 | A168B10C1BDBE6BD00646115 /* LoadingSample */, 86 | A168B1101BDBE9C100646115 /* PerfomanceTest */, 87 | A1EE65CB1A41F9DF00BEA147 /* Utils.swift */, 88 | 65D6FBC51A03859E00CF1DB7 /* black.jpg */, 89 | 65D6FBA61A03855700CF1DB7 /* Images.xcassets */, 90 | 65D6FBA81A03855700CF1DB7 /* LaunchScreen.xib */, 91 | 65D6FB9D1A03855700CF1DB7 /* Supporting Files */, 92 | A1B052581C03469500B67C0F /* Watchdog.swift */, 93 | ); 94 | path = ImageLoaderExample; 95 | sourceTree = ""; 96 | }; 97 | 65D6FB9D1A03855700CF1DB7 /* Supporting Files */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 65D6FB9E1A03855700CF1DB7 /* Info.plist */, 101 | ); 102 | name = "Supporting Files"; 103 | sourceTree = ""; 104 | }; 105 | A168B10C1BDBE6BD00646115 /* LoadingSample */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 65D6FBC11A03859500CF1DB7 /* SimpleViewController.swift */, 109 | A168B1081BDBDF9900646115 /* TableViewController.swift */, 110 | 659D34AA1AD7A2B800FEFABC /* CollectionViewController.swift */, 111 | ); 112 | name = LoadingSample; 113 | sourceTree = ""; 114 | }; 115 | A168B1101BDBE9C100646115 /* PerfomanceTest */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | A13D8A421C03498A00B59641 /* CPUAndMemoryPeformanceTestViewController.swift */, 119 | A13D8A441C0349F600B59641 /* BlockingMainThreadPerfomanceTestViewController.swift */, 120 | ); 121 | name = PerfomanceTest; 122 | sourceTree = ""; 123 | }; 124 | /* End PBXGroup section */ 125 | 126 | /* Begin PBXNativeTarget section */ 127 | 65D6FB991A03855700CF1DB7 /* ImageLoaderExample */ = { 128 | isa = PBXNativeTarget; 129 | buildConfigurationList = 65D6FBB91A03855800CF1DB7 /* Build configuration list for PBXNativeTarget "ImageLoaderExample" */; 130 | buildPhases = ( 131 | 65D6FB961A03855700CF1DB7 /* Sources */, 132 | 65D6FB971A03855700CF1DB7 /* Frameworks */, 133 | 65D6FB981A03855700CF1DB7 /* Resources */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = ImageLoaderExample; 140 | productName = ImageLoaderExample; 141 | productReference = 65D6FB9A1A03855700CF1DB7 /* ImageLoaderExample.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 65D6FB921A03855700CF1DB7 /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | LastSwiftUpdateCheck = 0700; 151 | LastUpgradeCheck = 1020; 152 | ORGANIZATIONNAME = "Hirohisa Kawasaki"; 153 | TargetAttributes = { 154 | 65D6FB991A03855700CF1DB7 = { 155 | CreatedOnToolsVersion = 6.1; 156 | LastSwiftMigration = 1020; 157 | }; 158 | }; 159 | }; 160 | buildConfigurationList = 65D6FB951A03855700CF1DB7 /* Build configuration list for PBXProject "ImageLoaderExample" */; 161 | compatibilityVersion = "Xcode 3.2"; 162 | developmentRegion = en; 163 | hasScannedForEncodings = 0; 164 | knownRegions = ( 165 | en, 166 | Base, 167 | ); 168 | mainGroup = 65D6FB911A03855700CF1DB7; 169 | productRefGroup = 65D6FB9B1A03855700CF1DB7 /* Products */; 170 | projectDirPath = ""; 171 | projectRoot = ""; 172 | targets = ( 173 | 65D6FB991A03855700CF1DB7 /* ImageLoaderExample */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXResourcesBuildPhase section */ 179 | 65D6FB981A03855700CF1DB7 /* Resources */ = { 180 | isa = PBXResourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 65AB21F71A5BE3C1007E6D40 /* Main.storyboard in Resources */, 184 | 65D6FBC61A03859E00CF1DB7 /* black.jpg in Resources */, 185 | 65D6FBA71A03855700CF1DB7 /* Images.xcassets in Resources */, 186 | 6578B7341A5BE1FE008D85D6 /* LaunchScreen.xib in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXResourcesBuildPhase section */ 191 | 192 | /* Begin PBXSourcesBuildPhase section */ 193 | 65D6FB961A03855700CF1DB7 /* Sources */ = { 194 | isa = PBXSourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | A13D8A431C03498A00B59641 /* CPUAndMemoryPeformanceTestViewController.swift in Sources */, 198 | A1EE65CC1A41F9DF00BEA147 /* Utils.swift in Sources */, 199 | A168B1091BDBDF9900646115 /* TableViewController.swift in Sources */, 200 | A1B052591C03469500B67C0F /* Watchdog.swift in Sources */, 201 | 65D6FBA01A03855700CF1DB7 /* AppDelegate.swift in Sources */, 202 | A13D8A451C0349F600B59641 /* BlockingMainThreadPerfomanceTestViewController.swift in Sources */, 203 | 659D34AB1AD7A2B800FEFABC /* CollectionViewController.swift in Sources */, 204 | 65D6FBC41A03859500CF1DB7 /* SimpleViewController.swift in Sources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXSourcesBuildPhase section */ 209 | 210 | /* Begin PBXVariantGroup section */ 211 | 65D6FBA31A03855700CF1DB7 /* Main.storyboard */ = { 212 | isa = PBXVariantGroup; 213 | children = ( 214 | 65D6FBA41A03855700CF1DB7 /* Base */, 215 | ); 216 | name = Main.storyboard; 217 | sourceTree = ""; 218 | }; 219 | 65D6FBA81A03855700CF1DB7 /* LaunchScreen.xib */ = { 220 | isa = PBXVariantGroup; 221 | children = ( 222 | 65D6FBA91A03855700CF1DB7 /* Base */, 223 | ); 224 | name = LaunchScreen.xib; 225 | sourceTree = ""; 226 | }; 227 | /* End PBXVariantGroup section */ 228 | 229 | /* Begin XCBuildConfiguration section */ 230 | 65D6FBB71A03855800CF1DB7 /* Debug */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 235 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 236 | CLANG_CXX_LIBRARY = "libc++"; 237 | CLANG_ENABLE_MODULES = YES; 238 | CLANG_ENABLE_OBJC_ARC = YES; 239 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 240 | CLANG_WARN_BOOL_CONVERSION = YES; 241 | CLANG_WARN_COMMA = YES; 242 | CLANG_WARN_CONSTANT_CONVERSION = YES; 243 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 244 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 245 | CLANG_WARN_EMPTY_BODY = YES; 246 | CLANG_WARN_ENUM_CONVERSION = YES; 247 | CLANG_WARN_INFINITE_RECURSION = YES; 248 | CLANG_WARN_INT_CONVERSION = YES; 249 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 251 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 254 | CLANG_WARN_STRICT_PROTOTYPES = YES; 255 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 259 | COPY_PHASE_STRIP = NO; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu99; 263 | GCC_DYNAMIC_NO_PIC = NO; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_OPTIMIZATION_LEVEL = 0; 266 | GCC_PREPROCESSOR_DEFINITIONS = ( 267 | "DEBUG=1", 268 | "$(inherited)", 269 | ); 270 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 278 | MTL_ENABLE_DEBUG_INFO = YES; 279 | ONLY_ACTIVE_ARCH = YES; 280 | SDKROOT = iphoneos; 281 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 282 | TARGETED_DEVICE_FAMILY = "1,2"; 283 | }; 284 | name = Debug; 285 | }; 286 | 65D6FBB81A03855800CF1DB7 /* Release */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 292 | CLANG_CXX_LIBRARY = "libc++"; 293 | CLANG_ENABLE_MODULES = YES; 294 | CLANG_ENABLE_OBJC_ARC = YES; 295 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 296 | CLANG_WARN_BOOL_CONVERSION = YES; 297 | CLANG_WARN_COMMA = YES; 298 | CLANG_WARN_CONSTANT_CONVERSION = YES; 299 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 300 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 301 | CLANG_WARN_EMPTY_BODY = YES; 302 | CLANG_WARN_ENUM_CONVERSION = YES; 303 | CLANG_WARN_INFINITE_RECURSION = YES; 304 | CLANG_WARN_INT_CONVERSION = YES; 305 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 307 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 309 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 310 | CLANG_WARN_STRICT_PROTOTYPES = YES; 311 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 315 | COPY_PHASE_STRIP = YES; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | GCC_C_LANGUAGE_STANDARD = gnu99; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 322 | GCC_WARN_UNDECLARED_SELECTOR = YES; 323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 324 | GCC_WARN_UNUSED_FUNCTION = YES; 325 | GCC_WARN_UNUSED_VARIABLE = YES; 326 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 327 | MTL_ENABLE_DEBUG_INFO = NO; 328 | SDKROOT = iphoneos; 329 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | VALIDATE_PRODUCT = YES; 332 | }; 333 | name = Release; 334 | }; 335 | 65D6FBBA1A03855800CF1DB7 /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 340 | INFOPLIST_FILE = ImageLoaderExample/Info.plist; 341 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 342 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | SWIFT_VERSION = 5.0; 345 | }; 346 | name = Debug; 347 | }; 348 | 65D6FBBB1A03855800CF1DB7 /* Release */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 352 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 353 | INFOPLIST_FILE = ImageLoaderExample/Info.plist; 354 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 355 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 356 | PRODUCT_NAME = "$(TARGET_NAME)"; 357 | SWIFT_VERSION = 5.0; 358 | }; 359 | name = Release; 360 | }; 361 | /* End XCBuildConfiguration section */ 362 | 363 | /* Begin XCConfigurationList section */ 364 | 65D6FB951A03855700CF1DB7 /* Build configuration list for PBXProject "ImageLoaderExample" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | 65D6FBB71A03855800CF1DB7 /* Debug */, 368 | 65D6FBB81A03855800CF1DB7 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | 65D6FBB91A03855800CF1DB7 /* Build configuration list for PBXNativeTarget "ImageLoaderExample" */ = { 374 | isa = XCConfigurationList; 375 | buildConfigurations = ( 376 | 65D6FBBA1A03855800CF1DB7 /* Debug */, 377 | 65D6FBBB1A03855800CF1DB7 /* Release */, 378 | ); 379 | defaultConfigurationIsVisible = 0; 380 | defaultConfigurationName = Release; 381 | }; 382 | /* End XCConfigurationList section */ 383 | }; 384 | rootObject = 65D6FB921A03855700CF1DB7 /* Project object */; 385 | } 386 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ImageLoaderSample 4 | // 5 | // Created by Hirohisa Kawasaki on 10/17/14. 6 | // Copyright (c) 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private let HOST_CPU_LOAD_INFO_COUNT = UInt32(MemoryLayout.size / MemoryLayout.size) 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 19 | 20 | return true 21 | 22 | } 23 | 24 | func report() { 25 | reportMemory() 26 | reportCPU() 27 | } 28 | 29 | func reportMemory() { 30 | } 31 | 32 | func reportCPU() { 33 | } 34 | 35 | private static let machHost = mach_host_self() 36 | 37 | } 38 | 39 | extension host_cpu_load_info { 40 | 41 | var cpuTicks: (user: natural_t, system: natural_t, idle: natural_t, nice: natural_t) { 42 | return cpu_ticks 43 | } 44 | 45 | var totalTick: UInt32 { 46 | let ticks = cpuTicks 47 | return ticks.user + ticks.system + ticks.idle + ticks.nice 48 | } 49 | 50 | var userUsageRatio: Double { 51 | return (Double(cpuTicks.user)/Double(totalTick)) * 100 52 | } 53 | var systemUsageRatio: Double { 54 | return (Double(cpuTicks.user)/Double(totalTick)) * 100 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 220 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/BlockingMainThreadPerfomanceTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockingMainThreadPerfomanceTestViewController.swift 3 | // ImageLoaderExample 4 | // 5 | // Created by Hirohisa Kawasaki on 11/23/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImageLoader 11 | 12 | class BlockingMainThreadPerfomanceTestViewController: CollectionViewController { 13 | 14 | var watchdog: Watchdog? 15 | func report() { 16 | let delegate = UIApplication.shared.delegate as! AppDelegate 17 | delegate.report() 18 | } 19 | 20 | override func viewWillAppear(_ animated: Bool) { 21 | super.viewWillAppear(animated) 22 | Disk().cleanUp() 23 | } 24 | 25 | override func viewDidAppear(_ animated: Bool) { 26 | super.viewDidAppear(animated) 27 | watchdog = Watchdog(threshold: 0.1, handler: { duration in 28 | print("👮 Main thread was blocked for " + String(format:"%.2f", duration) + "s 👮") 29 | }) 30 | } 31 | 32 | override func viewDidDisappear(_ animated: Bool) { 33 | super.viewDidDisappear(animated) 34 | watchdog = nil 35 | } 36 | 37 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 38 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell 39 | 40 | let url = String.imageURL(indexPath.row % 100) 41 | 42 | let startDate = Date() 43 | cell.imageView.contentMode = contentMode 44 | cell.imageView.image = UIImage(color: UIColor.black) 45 | cell.imageView.load.request(with: url, onCompletion: { _, _, operation in 46 | switch operation { 47 | case .network: 48 | let diff = Date().timeIntervalSince(startDate) 49 | print("loading time: \(diff)") 50 | if let image = cell.imageView.image { 51 | print("from network, image size: \(image.size)") 52 | } 53 | case .disk: 54 | if let image = cell.imageView.image { 55 | print("from cache, image size: \(image.size)") 56 | } 57 | 58 | case .error: 59 | print("error") 60 | } 61 | }) 62 | 63 | return cell 64 | } 65 | 66 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 67 | return 200 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/CPUAndMemoryPeformanceTestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CPUAndMemoryPeformanceTestViewController.swift 3 | // ImageLoaderExample 4 | // 5 | // Created by Hirohisa Kawasaki on 11/23/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImageLoader 11 | 12 | class CPUAndMemoryPeformanceTestViewController: CollectionViewController { 13 | 14 | var timer: Timer? 15 | @objc func report() { 16 | let delegate = UIApplication.shared.delegate as! AppDelegate 17 | delegate.report() 18 | } 19 | 20 | override func viewWillAppear(_ animated: Bool) { 21 | super.viewWillAppear(animated) 22 | Disk().cleanUp() 23 | } 24 | 25 | override func viewDidAppear(_ animated: Bool) { 26 | super.viewDidAppear(animated) 27 | timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(CPUAndMemoryPeformanceTestViewController.report), userInfo: nil, repeats: true) 28 | RunLoop.main.add(timer!, forMode: RunLoop.Mode.common) 29 | } 30 | 31 | override func viewDidDisappear(_ animated: Bool) { 32 | super.viewDidDisappear(animated) 33 | timer?.invalidate() 34 | timer = nil 35 | } 36 | 37 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 38 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell 39 | 40 | let url = String.imageURL(indexPath.row % 100) 41 | 42 | cell.imageView.contentMode = contentMode 43 | cell.imageView.image = UIImage(color: UIColor.black) 44 | cell.imageView.load.request(with: url, options: [.adjustSize]) 45 | 46 | return cell 47 | } 48 | 49 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 50 | return 200 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // ImageLoaderExample 4 | // 5 | // Created by Hirohisa Kawasaki on 4/10/15. 6 | // Copyright (c) 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CollectionViewCell: UICollectionViewCell { 12 | @IBOutlet weak var imageView: UIImageView! 13 | } 14 | 15 | class CollectionViewController: UICollectionViewController { 16 | 17 | var contentMode: UIView.ContentMode = UIView.ContentMode.scaleToFill { 18 | didSet { 19 | reloadData() 20 | } 21 | } 22 | 23 | let modeMap: [UIView.ContentMode: UIView.ContentMode] = [ 24 | .scaleToFill: .scaleAspectFit, 25 | .scaleAspectFit: .scaleAspectFill, 26 | .scaleAspectFill: .scaleToFill, 27 | ] 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Change", style: .plain, target: self, action: #selector(CollectionViewController.changeContentMode)) 33 | reloadData() 34 | } 35 | 36 | @objc func changeContentMode() { 37 | contentMode = modeMap[contentMode]! 38 | } 39 | 40 | func reloadData() { 41 | switch contentMode { 42 | case .scaleToFill: 43 | title = "ScaleToFill" 44 | case .scaleAspectFit: 45 | title = "ScaleAspectFit" 46 | case .scaleAspectFill: 47 | title = "ScaleAspectFill" 48 | default: 49 | break 50 | } 51 | 52 | collectionView?.reloadData() 53 | } 54 | 55 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 56 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell 57 | 58 | let url = String.imageURL(indexPath.row) 59 | cell.imageView.contentMode = contentMode 60 | cell.imageView.load.request(with: url) 61 | 62 | return cell 63 | } 64 | 65 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 66 | return 50 67 | } 68 | 69 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 70 | return 1 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/ImageLoaderExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | NSAppTransportSecurity 47 | 48 | NSAllowsArbitraryLoads 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/SimpleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleViewController.swift 3 | // ImageLoaderSample 4 | // 5 | // Created by Hirohisa Kawasaki on 10/17/14. 6 | // Copyright (c) 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImageLoader 11 | 12 | class SimpleViewController: UIViewController { 13 | 14 | @IBOutlet weak var imageView: UIImageView! 15 | 16 | @IBOutlet weak var successURLButton: UIButton! 17 | @IBOutlet weak var failureURLButton: UIButton! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | title = "Simple" 22 | 23 | imageView.backgroundColor = UIColor.black 24 | } 25 | 26 | // MARK: - try 27 | 28 | @IBAction func tryLoadSuccessURL() { 29 | let string = "https://www.google.co.jp/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" 30 | tryLoad(URL(string: string)!) 31 | } 32 | 33 | @IBAction func tryLoadFailureURL() { 34 | let string = "http://upload.wikimedia.org/wikipedia/commons/1/1b/Bachalpseeflowers.jpg" 35 | tryLoad(URL(string: string)!) 36 | } 37 | 38 | func tryLoad(_ url: URL) { 39 | imageView.load.request(with: url, onCompletion: { _, error, _ in 40 | print("error \(String(describing: error))") 41 | }) 42 | } 43 | 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // ImageLoaderExample 4 | // 5 | // Created by Hirohisa Kawasaki on 10/25/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImageLoader 11 | 12 | class TableViewCell: UITableViewCell { 13 | @IBOutlet weak var thumbnailView: UIImageView! 14 | } 15 | 16 | class TableViewController: UITableViewController { 17 | 18 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 19 | 20 | let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell 21 | 22 | let url = String.imageURL(indexPath.row) 23 | cell.thumbnailView.image = UIImage(named: "black.jpg") 24 | cell.thumbnailView.load.request(with: url, onCompletion: { image, error, operation in 25 | print("image \(String(describing: image?.size)), render-image \(String(describing: cell.thumbnailView.image?.size))") 26 | if operation == .network { 27 | let transition = CATransition() 28 | transition.duration = 0.5 29 | transition.type = CATransitionType.fade 30 | cell.thumbnailView.layer.add(transition, forKey: nil) 31 | cell.thumbnailView.image = image 32 | } 33 | }) 34 | 35 | return cell 36 | 37 | } 38 | 39 | override func numberOfSections(in tableView: UITableView) -> Int { 40 | return 1 41 | } 42 | 43 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 44 | return 100 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // ImageLoaderExample 4 | // 5 | // Created by Hirohisa Kawasaki on 12/18/14. 6 | // Copyright (c) 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension String { 13 | 14 | static func imageURL(_ index: Int) -> String { 15 | 16 | var number = index.description 17 | while (number.count < 3) { 18 | number = "0\(number)" 19 | } 20 | return "https://s3.amazonaws.com/fast-image-cache/demo-images/FICDDemoImage\(number).jpg" 21 | } 22 | 23 | } 24 | 25 | extension URL { 26 | 27 | static func imageURL(_ index: Int) -> URL { 28 | 29 | var number = index.description 30 | while (number.count < 3) { 31 | number = "0\(number)" 32 | } 33 | let string = "https://s3.amazonaws.com/fast-image-cache/demo-images/FICDDemoImage\(number).jpg" 34 | 35 | return URL(string: string)! 36 | } 37 | 38 | } 39 | 40 | extension UIImage { 41 | public convenience init?(color: UIColor) { 42 | self.init(color: color, size: CGSize(width: 1, height: 1)) 43 | } 44 | 45 | public convenience init?(color: UIColor, size: CGSize) { 46 | let frameFor1px = CGRect(x: 0, y: 0, width: size.width, height: size.height) 47 | UIGraphicsBeginImageContext(frameFor1px.size) 48 | let context = UIGraphicsGetCurrentContext() 49 | context?.setFillColor(color.cgColor) 50 | context?.fill(frameFor1px) 51 | 52 | let image = UIGraphicsGetImageFromCurrentImageContext() 53 | UIGraphicsEndImageContext() 54 | 55 | guard let cgImage = image!.cgImage else { return nil } 56 | 57 | self.init(cgImage: cgImage) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/Watchdog.swift: -------------------------------------------------------------------------------- 1 | // 2 | //https://github.com/wojteklu/Watchdog 3 | // 4 | //The MIT License (MIT) 5 | // 6 | //Copyright (c) 2015 Wojtek Lukaszuk 7 | // 8 | //Permission is hereby granted, free of charge, to any person obtaining a copy 9 | //of this software and associated documentation files (the "Software"), to deal 10 | //in the Software without restriction, including without limitation the rights 11 | //to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | //copies of the Software, and to permit persons to whom the Software is 13 | //furnished to do so, subject to the following conditions: 14 | // 15 | //The above copyright notice and this permission notice shall be included in all 16 | //copies or substantial portions of the Software. 17 | // 18 | //THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | //IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | //FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | //AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | //SOFTWARE. 25 | 26 | import Foundation 27 | 28 | @objc public class Watchdog: NSObject { 29 | 30 | /* 31 | The number of seconds that must pass to consider the main thread blocked 32 | */ 33 | private var threshold: Double 34 | 35 | private var runLoop: CFRunLoop = CFRunLoopGetMain() 36 | private var observer: CFRunLoopObserver! 37 | private var startTime: UInt64 = 0 38 | private var handler: ((Double) -> ())? = nil 39 | 40 | public init(threshold: Double = 0.2, handler: ((Double) -> ())? = nil) { 41 | 42 | self.threshold = threshold 43 | self.handler = handler 44 | super.init() 45 | 46 | var timebase: mach_timebase_info_data_t = mach_timebase_info(numer: 0, denom: 0) 47 | mach_timebase_info(&timebase) 48 | let secondsPerMachine: TimeInterval = TimeInterval(Double(timebase.numer) / Double(timebase.denom) / Double(1e9)) 49 | 50 | observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, 51 | CFRunLoopActivity.allActivities.rawValue, 52 | true, 53 | 0) { [weak self] (observer, activity) in 54 | 55 | guard let weakSelf = self else { return } 56 | 57 | switch(activity) { 58 | 59 | case CFRunLoopActivity.entry, CFRunLoopActivity.beforeTimers, 60 | CFRunLoopActivity.afterWaiting, CFRunLoopActivity.beforeSources: 61 | 62 | if weakSelf.startTime == 0 { 63 | weakSelf.startTime = mach_absolute_time() 64 | } 65 | 66 | case CFRunLoopActivity.beforeWaiting, CFRunLoopActivity.exit: 67 | 68 | let elapsed = mach_absolute_time() - weakSelf.startTime 69 | let duration: TimeInterval = TimeInterval(elapsed) * secondsPerMachine 70 | 71 | if duration > weakSelf.threshold { 72 | if let handler = weakSelf.handler { 73 | handler(duration) 74 | } else { 75 | print("👮 Main thread was blocked for " + String(format:"%.2f", duration) + "s 👮") 76 | } 77 | } 78 | 79 | weakSelf.startTime = 0 80 | 81 | default: () 82 | 83 | } 84 | } 85 | 86 | CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes) 87 | } 88 | 89 | deinit { 90 | CFRunLoopObserverInvalidate(observer) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Example/ImageLoaderExample/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirohisa/ImageLoaderSwift/f86e15e14f7f6088e9a2171821e60bf4f701a0d7/Example/ImageLoaderExample/black.jpg -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods', '1.1.1' 4 | gem 'xcpretty', '0.2.2' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.6) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.2) 11 | claide (1.0.2) 12 | cocoapods (1.1.1) 13 | activesupport (>= 4.0.2, < 5) 14 | claide (>= 1.0.1, < 2.0) 15 | cocoapods-core (= 1.1.1) 16 | cocoapods-deintegrate (>= 1.0.1, < 2.0) 17 | cocoapods-downloader (>= 1.1.2, < 2.0) 18 | cocoapods-plugins (>= 1.0.0, < 2.0) 19 | cocoapods-search (>= 1.0.0, < 2.0) 20 | cocoapods-stats (>= 1.0.0, < 2.0) 21 | cocoapods-trunk (>= 1.1.1, < 2.0) 22 | cocoapods-try (>= 1.1.0, < 2.0) 23 | colored (~> 1.2) 24 | escape (~> 0.0.4) 25 | fourflusher (~> 2.0.1) 26 | gh_inspector (~> 1.0) 27 | molinillo (~> 0.5.1) 28 | nap (~> 1.0) 29 | xcodeproj (>= 1.3.3, < 2.0) 30 | cocoapods-core (1.1.1) 31 | activesupport (>= 4.0.2, < 5) 32 | fuzzy_match (~> 2.0.4) 33 | nap (~> 1.0) 34 | cocoapods-deintegrate (1.0.2) 35 | cocoapods-downloader (1.1.3) 36 | cocoapods-plugins (1.0.0) 37 | nap 38 | cocoapods-search (1.0.0) 39 | cocoapods-stats (1.0.0) 40 | cocoapods-trunk (1.3.0) 41 | nap (>= 0.8, < 2.0) 42 | netrc (~> 0.11) 43 | cocoapods-try (1.1.0) 44 | colored (1.2) 45 | colored2 (3.1.2) 46 | concurrent-ruby (1.0.5) 47 | escape (0.0.4) 48 | fourflusher (2.0.1) 49 | fuzzy_match (2.0.4) 50 | gh_inspector (1.1.3) 51 | i18n (0.9.5) 52 | concurrent-ruby (~> 1.0) 53 | minitest (5.11.3) 54 | molinillo (0.5.7) 55 | nanaimo (0.2.3) 56 | nap (1.1.0) 57 | netrc (0.11.0) 58 | rouge (1.11.1) 59 | thread_safe (0.3.6) 60 | tzinfo (1.2.5) 61 | thread_safe (~> 0.1) 62 | xcodeproj (1.5.6) 63 | CFPropertyList (~> 2.3.3) 64 | atomos (~> 0.1.2) 65 | claide (>= 1.0.2, < 2.0) 66 | colored2 (~> 3.1) 67 | nanaimo (~> 0.2.3) 68 | xcpretty (0.2.2) 69 | rouge (~> 1.8) 70 | 71 | PLATFORMS 72 | ruby 73 | 74 | DEPENDENCIES 75 | cocoapods (= 1.1.1) 76 | xcpretty (= 0.2.2) 77 | 78 | BUNDLED WITH 79 | 1.16.1 80 | -------------------------------------------------------------------------------- /ImageLoader.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "ImageLoader" 4 | s.version = "0.15.0" 5 | s.summary = "A lightweight and fast image loader for iOS written in Swift." 6 | s.description = <<-DESC 7 | ImageLoader is an instrument for asynchronous image loading written in Swift. It is a lightweight and fast image loader for iOS. 8 | DESC 9 | 10 | s.homepage = "https://github.com/hirohisa/ImageLoaderSwift" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | 13 | s.author = { "Hirohisa Kawasaki" => "hirohisa.kawasaki@gmail.com" } 14 | 15 | s.source = { :git => "https://github.com/hirohisa/ImageLoaderSwift.git", :tag => s.version } 16 | 17 | s.source_files = "ImageLoader/**/*.swift" 18 | s.requires_arc = true 19 | s.ios.deployment_target = '10.0' 20 | end 21 | -------------------------------------------------------------------------------- /ImageLoader.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 650F99481C0EC45D00540E8D /* black.png in Resources */ = {isa = PBXBuildFile; fileRef = 650F99471C0EC45D00540E8D /* black.png */; }; 11 | 650F994A1C0EC46300540E8D /* white.png in Resources */ = {isa = PBXBuildFile; fileRef = 650F99491C0EC46300540E8D /* white.png */; }; 12 | 6529075D1BA19D5F002B8D89 /* ImageLoader.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 65B2CF1119EF9F6600DA3605 /* ImageLoader.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 13 | 6535D0801DDC2E8100A197F0 /* UIImage+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6535D07F1DDC2E8100A197F0 /* UIImage+ImageLoader.swift */; }; 14 | 6535D0821DDC3B3400A197F0 /* Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6535D0811DDC3B3400A197F0 /* Option.swift */; }; 15 | 65B2CF1719EF9F6600DA3605 /* ImageLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 65B2CF1619EF9F6600DA3605 /* ImageLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | 65B2CF1D19EF9F6600DA3605 /* ImageLoader.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65B2CF1119EF9F6600DA3605 /* ImageLoader.framework */; }; 17 | 65DF69211DCCB2D400B649AE /* DiskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF691A1DCCB2D400B649AE /* DiskTests.swift */; }; 18 | 65DF69221DCCB2D400B649AE /* ImageLoaderTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF691B1DCCB2D400B649AE /* ImageLoaderTestCase.swift */; }; 19 | 65DF69241DCCB2D400B649AE /* ImageLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF691D1DCCB2D400B649AE /* ImageLoaderTests.swift */; }; 20 | 65DF69251DCCB2D400B649AE /* UIImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF691E1DCCB2D400B649AE /* UIImageTests.swift */; }; 21 | 65DF69261DCCB2D400B649AE /* UIImageViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF691F1DCCB2D400B649AE /* UIImageViewTests.swift */; }; 22 | 65DF69271DCCB2D400B649AE /* URLLiteralConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF69201DCCB2D400B649AE /* URLLiteralConvertibleTests.swift */; }; 23 | 65DF692A1DD0453200B649AE /* HashStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF69291DD0453200B649AE /* HashStorage.swift */; }; 24 | 65DF692C1DD0527E00B649AE /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65DF692B1DD0527E00B649AE /* ImageLoader.swift */; }; 25 | A11F984F2085E145000656A2 /* Data+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11F984E2085E145000656A2 /* Data+ImageLoader.swift */; }; 26 | A11F98512085EA96000656A2 /* CGImageSource+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11F98502085EA96000656A2 /* CGImageSource+ImageLoader.swift */; }; 27 | A1DC71DF1DCF88DD009712A5 /* Disk.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71D91DCF88DD009712A5 /* Disk.swift */; }; 28 | A1DC71E11DCF88DD009712A5 /* Loadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71DB1DCF88DD009712A5 /* Loadable.swift */; }; 29 | A1DC71E21DCF88DD009712A5 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71DC1DCF88DD009712A5 /* Loader.swift */; }; 30 | A1DC71E31DCF88DD009712A5 /* Operative.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71DD1DCF88DD009712A5 /* Operative.swift */; }; 31 | A1DC71E41DCF88DD009712A5 /* URLLiteralConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71DE1DCF88DD009712A5 /* URLLiteralConvertible.swift */; }; 32 | A1DC71E71DCF88E3009712A5 /* UIImageView+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DC71E61DCF88E3009712A5 /* UIImageView+ImageLoader.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 65B2CF1E19EF9F6600DA3605 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 65B2CF0819EF9F6600DA3605 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 65B2CF1019EF9F6600DA3605; 41 | remoteInfo = ImageLoader; 42 | }; 43 | /* End PBXContainerItemProxy section */ 44 | 45 | /* Begin PBXCopyFilesBuildPhase section */ 46 | 6529075A1BA19BD7002B8D89 /* Copy Files */ = { 47 | isa = PBXCopyFilesBuildPhase; 48 | buildActionMask = 2147483647; 49 | dstPath = ""; 50 | dstSubfolderSpec = 10; 51 | files = ( 52 | 6529075D1BA19D5F002B8D89 /* ImageLoader.framework in Copy Files */, 53 | ); 54 | name = "Copy Files"; 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | /* End PBXCopyFilesBuildPhase section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | 0D1A241FE45F6F50C4254DF0 /* Pods_ImageLoaderTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ImageLoaderTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 650F99471C0EC45D00540E8D /* black.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = black.png; sourceTree = ""; }; 62 | 650F99491C0EC46300540E8D /* white.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = white.png; sourceTree = ""; }; 63 | 6535D07F1DDC2E8100A197F0 /* UIImage+ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+ImageLoader.swift"; sourceTree = ""; }; 64 | 6535D0811DDC3B3400A197F0 /* Option.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Option.swift; sourceTree = ""; }; 65 | 65B2CF1119EF9F6600DA3605 /* ImageLoader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ImageLoader.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | 65B2CF1519EF9F6600DA3605 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 65B2CF1619EF9F6600DA3605 /* ImageLoader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImageLoader.h; sourceTree = ""; }; 68 | 65B2CF1C19EF9F6600DA3605 /* ImageLoaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImageLoaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | 65B2CF2219EF9F6600DA3605 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 65DF691A1DCCB2D400B649AE /* DiskTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskTests.swift; sourceTree = ""; }; 71 | 65DF691B1DCCB2D400B649AE /* ImageLoaderTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoaderTestCase.swift; sourceTree = ""; }; 72 | 65DF691D1DCCB2D400B649AE /* ImageLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoaderTests.swift; sourceTree = ""; }; 73 | 65DF691E1DCCB2D400B649AE /* UIImageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageTests.swift; sourceTree = ""; }; 74 | 65DF691F1DCCB2D400B649AE /* UIImageViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewTests.swift; sourceTree = ""; }; 75 | 65DF69201DCCB2D400B649AE /* URLLiteralConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLLiteralConvertibleTests.swift; sourceTree = ""; }; 76 | 65DF69291DD0453200B649AE /* HashStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashStorage.swift; sourceTree = ""; }; 77 | 65DF692B1DD0527E00B649AE /* ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; 78 | A11F984E2085E145000656A2 /* Data+ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+ImageLoader.swift"; sourceTree = ""; }; 79 | A11F98502085EA96000656A2 /* CGImageSource+ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImageSource+ImageLoader.swift"; sourceTree = ""; }; 80 | A1DC71D91DCF88DD009712A5 /* Disk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Disk.swift; sourceTree = ""; }; 81 | A1DC71DB1DCF88DD009712A5 /* Loadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loadable.swift; sourceTree = ""; }; 82 | A1DC71DC1DCF88DD009712A5 /* Loader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; 83 | A1DC71DD1DCF88DD009712A5 /* Operative.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operative.swift; sourceTree = ""; }; 84 | A1DC71DE1DCF88DD009712A5 /* URLLiteralConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLLiteralConvertible.swift; sourceTree = ""; }; 85 | A1DC71E61DCF88E3009712A5 /* UIImageView+ImageLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+ImageLoader.swift"; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 65B2CF0D19EF9F6600DA3605 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | 65B2CF1919EF9F6600DA3605 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | 65B2CF1D19EF9F6600DA3605 /* ImageLoader.framework in Frameworks */, 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | /* End PBXFrameworksBuildPhase section */ 105 | 106 | /* Begin PBXGroup section */ 107 | 4A750151C404408BDF8F46F1 /* Frameworks */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 0D1A241FE45F6F50C4254DF0 /* Pods_ImageLoaderTests.framework */, 111 | ); 112 | name = Frameworks; 113 | sourceTree = ""; 114 | }; 115 | 65B2CF0719EF9F6600DA3605 = { 116 | isa = PBXGroup; 117 | children = ( 118 | 65B2CF1319EF9F6600DA3605 /* ImageLoader */, 119 | 65B2CF2019EF9F6600DA3605 /* ImageLoaderTests */, 120 | 65B2CF1219EF9F6600DA3605 /* Products */, 121 | 4A750151C404408BDF8F46F1 /* Frameworks */, 122 | ); 123 | sourceTree = ""; 124 | }; 125 | 65B2CF1219EF9F6600DA3605 /* Products */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 65B2CF1119EF9F6600DA3605 /* ImageLoader.framework */, 129 | 65B2CF1C19EF9F6600DA3605 /* ImageLoaderTests.xctest */, 130 | ); 131 | name = Products; 132 | sourceTree = ""; 133 | }; 134 | 65B2CF1319EF9F6600DA3605 /* ImageLoader */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | A1DC71E51DCF88E3009712A5 /* Extension */, 138 | A1DC71D81DCF88DD009712A5 /* Core */, 139 | 65B2CF1619EF9F6600DA3605 /* ImageLoader.h */, 140 | 65B2CF1419EF9F6600DA3605 /* Supporting Files */, 141 | ); 142 | path = ImageLoader; 143 | sourceTree = ""; 144 | }; 145 | 65B2CF1419EF9F6600DA3605 /* Supporting Files */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 65B2CF1519EF9F6600DA3605 /* Info.plist */, 149 | ); 150 | name = "Supporting Files"; 151 | sourceTree = ""; 152 | }; 153 | 65B2CF2019EF9F6600DA3605 /* ImageLoaderTests */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 65DF691B1DCCB2D400B649AE /* ImageLoaderTestCase.swift */, 157 | 65DF691A1DCCB2D400B649AE /* DiskTests.swift */, 158 | 65DF691D1DCCB2D400B649AE /* ImageLoaderTests.swift */, 159 | 65DF691E1DCCB2D400B649AE /* UIImageTests.swift */, 160 | 65DF691F1DCCB2D400B649AE /* UIImageViewTests.swift */, 161 | 65DF69201DCCB2D400B649AE /* URLLiteralConvertibleTests.swift */, 162 | 65B2CF2119EF9F6600DA3605 /* Supporting Files */, 163 | ); 164 | path = ImageLoaderTests; 165 | sourceTree = ""; 166 | }; 167 | 65B2CF2119EF9F6600DA3605 /* Supporting Files */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 650F99471C0EC45D00540E8D /* black.png */, 171 | 650F99491C0EC46300540E8D /* white.png */, 172 | 65B2CF2219EF9F6600DA3605 /* Info.plist */, 173 | ); 174 | name = "Supporting Files"; 175 | sourceTree = ""; 176 | }; 177 | A1DC71D81DCF88DD009712A5 /* Core */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | A1DC71D91DCF88DD009712A5 /* Disk.swift */, 181 | 65DF692B1DD0527E00B649AE /* ImageLoader.swift */, 182 | A1DC71DB1DCF88DD009712A5 /* Loadable.swift */, 183 | A1DC71DC1DCF88DD009712A5 /* Loader.swift */, 184 | A1DC71DD1DCF88DD009712A5 /* Operative.swift */, 185 | A1DC71DE1DCF88DD009712A5 /* URLLiteralConvertible.swift */, 186 | 65DF69291DD0453200B649AE /* HashStorage.swift */, 187 | 6535D0811DDC3B3400A197F0 /* Option.swift */, 188 | ); 189 | path = Core; 190 | sourceTree = ""; 191 | }; 192 | A1DC71E51DCF88E3009712A5 /* Extension */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | A1DC71E61DCF88E3009712A5 /* UIImageView+ImageLoader.swift */, 196 | 6535D07F1DDC2E8100A197F0 /* UIImage+ImageLoader.swift */, 197 | A11F984E2085E145000656A2 /* Data+ImageLoader.swift */, 198 | A11F98502085EA96000656A2 /* CGImageSource+ImageLoader.swift */, 199 | ); 200 | path = Extension; 201 | sourceTree = ""; 202 | }; 203 | /* End PBXGroup section */ 204 | 205 | /* Begin PBXHeadersBuildPhase section */ 206 | 65B2CF0E19EF9F6600DA3605 /* Headers */ = { 207 | isa = PBXHeadersBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 65B2CF1719EF9F6600DA3605 /* ImageLoader.h in Headers */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXHeadersBuildPhase section */ 215 | 216 | /* Begin PBXNativeTarget section */ 217 | 65B2CF1019EF9F6600DA3605 /* ImageLoader */ = { 218 | isa = PBXNativeTarget; 219 | buildConfigurationList = 65B2CF2719EF9F6600DA3605 /* Build configuration list for PBXNativeTarget "ImageLoader" */; 220 | buildPhases = ( 221 | 65B2CF0C19EF9F6600DA3605 /* Sources */, 222 | 65B2CF0D19EF9F6600DA3605 /* Frameworks */, 223 | 65B2CF0E19EF9F6600DA3605 /* Headers */, 224 | 65B2CF0F19EF9F6600DA3605 /* Resources */, 225 | ); 226 | buildRules = ( 227 | ); 228 | dependencies = ( 229 | ); 230 | name = ImageLoader; 231 | productName = ImageLoader; 232 | productReference = 65B2CF1119EF9F6600DA3605 /* ImageLoader.framework */; 233 | productType = "com.apple.product-type.framework"; 234 | }; 235 | 65B2CF1B19EF9F6600DA3605 /* ImageLoaderTests */ = { 236 | isa = PBXNativeTarget; 237 | buildConfigurationList = 65B2CF2A19EF9F6600DA3605 /* Build configuration list for PBXNativeTarget "ImageLoaderTests" */; 238 | buildPhases = ( 239 | 65B2CF1819EF9F6600DA3605 /* Sources */, 240 | 65B2CF1919EF9F6600DA3605 /* Frameworks */, 241 | 65B2CF1A19EF9F6600DA3605 /* Resources */, 242 | 6529075A1BA19BD7002B8D89 /* Copy Files */, 243 | ); 244 | buildRules = ( 245 | ); 246 | dependencies = ( 247 | 65B2CF1F19EF9F6600DA3605 /* PBXTargetDependency */, 248 | ); 249 | name = ImageLoaderTests; 250 | productName = ImageLoaderTests; 251 | productReference = 65B2CF1C19EF9F6600DA3605 /* ImageLoaderTests.xctest */; 252 | productType = "com.apple.product-type.bundle.unit-test"; 253 | }; 254 | /* End PBXNativeTarget section */ 255 | 256 | /* Begin PBXProject section */ 257 | 65B2CF0819EF9F6600DA3605 /* Project object */ = { 258 | isa = PBXProject; 259 | attributes = { 260 | LastSwiftMigration = 0700; 261 | LastSwiftUpdateCheck = 0700; 262 | LastUpgradeCheck = 1020; 263 | ORGANIZATIONNAME = "Hirohisa Kawasaki"; 264 | TargetAttributes = { 265 | 65B2CF1019EF9F6600DA3605 = { 266 | CreatedOnToolsVersion = 6.1; 267 | LastSwiftMigration = 1020; 268 | }; 269 | 65B2CF1B19EF9F6600DA3605 = { 270 | CreatedOnToolsVersion = 6.1; 271 | LastSwiftMigration = 1020; 272 | }; 273 | }; 274 | }; 275 | buildConfigurationList = 65B2CF0B19EF9F6600DA3605 /* Build configuration list for PBXProject "ImageLoader" */; 276 | compatibilityVersion = "Xcode 3.2"; 277 | developmentRegion = en; 278 | hasScannedForEncodings = 0; 279 | knownRegions = ( 280 | en, 281 | Base, 282 | ); 283 | mainGroup = 65B2CF0719EF9F6600DA3605; 284 | productRefGroup = 65B2CF1219EF9F6600DA3605 /* Products */; 285 | projectDirPath = ""; 286 | projectRoot = ""; 287 | targets = ( 288 | 65B2CF1019EF9F6600DA3605 /* ImageLoader */, 289 | 65B2CF1B19EF9F6600DA3605 /* ImageLoaderTests */, 290 | ); 291 | }; 292 | /* End PBXProject section */ 293 | 294 | /* Begin PBXResourcesBuildPhase section */ 295 | 65B2CF0F19EF9F6600DA3605 /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | 65B2CF1A19EF9F6600DA3605 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | 650F99481C0EC45D00540E8D /* black.png in Resources */, 307 | 650F994A1C0EC46300540E8D /* white.png in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | /* End PBXResourcesBuildPhase section */ 312 | 313 | /* Begin PBXSourcesBuildPhase section */ 314 | 65B2CF0C19EF9F6600DA3605 /* Sources */ = { 315 | isa = PBXSourcesBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | A11F984F2085E145000656A2 /* Data+ImageLoader.swift in Sources */, 319 | A1DC71E21DCF88DD009712A5 /* Loader.swift in Sources */, 320 | 6535D0801DDC2E8100A197F0 /* UIImage+ImageLoader.swift in Sources */, 321 | 6535D0821DDC3B3400A197F0 /* Option.swift in Sources */, 322 | 65DF692C1DD0527E00B649AE /* ImageLoader.swift in Sources */, 323 | A1DC71E41DCF88DD009712A5 /* URLLiteralConvertible.swift in Sources */, 324 | 65DF692A1DD0453200B649AE /* HashStorage.swift in Sources */, 325 | A1DC71E31DCF88DD009712A5 /* Operative.swift in Sources */, 326 | A1DC71DF1DCF88DD009712A5 /* Disk.swift in Sources */, 327 | A11F98512085EA96000656A2 /* CGImageSource+ImageLoader.swift in Sources */, 328 | A1DC71E11DCF88DD009712A5 /* Loadable.swift in Sources */, 329 | A1DC71E71DCF88E3009712A5 /* UIImageView+ImageLoader.swift in Sources */, 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | 65B2CF1819EF9F6600DA3605 /* Sources */ = { 334 | isa = PBXSourcesBuildPhase; 335 | buildActionMask = 2147483647; 336 | files = ( 337 | 65DF69211DCCB2D400B649AE /* DiskTests.swift in Sources */, 338 | 65DF69241DCCB2D400B649AE /* ImageLoaderTests.swift in Sources */, 339 | 65DF69261DCCB2D400B649AE /* UIImageViewTests.swift in Sources */, 340 | 65DF69221DCCB2D400B649AE /* ImageLoaderTestCase.swift in Sources */, 341 | 65DF69271DCCB2D400B649AE /* URLLiteralConvertibleTests.swift in Sources */, 342 | 65DF69251DCCB2D400B649AE /* UIImageTests.swift in Sources */, 343 | ); 344 | runOnlyForDeploymentPostprocessing = 0; 345 | }; 346 | /* End PBXSourcesBuildPhase section */ 347 | 348 | /* Begin PBXTargetDependency section */ 349 | 65B2CF1F19EF9F6600DA3605 /* PBXTargetDependency */ = { 350 | isa = PBXTargetDependency; 351 | target = 65B2CF1019EF9F6600DA3605 /* ImageLoader */; 352 | targetProxy = 65B2CF1E19EF9F6600DA3605 /* PBXContainerItemProxy */; 353 | }; 354 | /* End PBXTargetDependency section */ 355 | 356 | /* Begin XCBuildConfiguration section */ 357 | 65B2CF2519EF9F6600DA3605 /* Debug */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 381 | CLANG_WARN_STRICT_PROTOTYPES = YES; 382 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 383 | CLANG_WARN_UNREACHABLE_CODE = YES; 384 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 385 | CODE_SIGN_IDENTITY = "iPhone Developer"; 386 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 387 | COPY_PHASE_STRIP = NO; 388 | CURRENT_PROJECT_VERSION = 1; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | ENABLE_TESTABILITY = YES; 391 | GCC_C_LANGUAGE_STANDARD = gnu99; 392 | GCC_DYNAMIC_NO_PIC = NO; 393 | GCC_NO_COMMON_BLOCKS = YES; 394 | GCC_OPTIMIZATION_LEVEL = 0; 395 | GCC_PREPROCESSOR_DEFINITIONS = ( 396 | "DEBUG=1", 397 | "$(inherited)", 398 | ); 399 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 407 | MTL_ENABLE_DEBUG_INFO = YES; 408 | ONLY_ACTIVE_ARCH = YES; 409 | SDKROOT = iphoneos; 410 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | VERSIONING_SYSTEM = "apple-generic"; 413 | VERSION_INFO_PREFIX = ""; 414 | }; 415 | name = Debug; 416 | }; 417 | 65B2CF2619EF9F6600DA3605 /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ALWAYS_SEARCH_USER_PATHS = NO; 421 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 422 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 423 | CLANG_CXX_LIBRARY = "libc++"; 424 | CLANG_ENABLE_MODULES = YES; 425 | CLANG_ENABLE_OBJC_ARC = YES; 426 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 427 | CLANG_WARN_BOOL_CONVERSION = YES; 428 | CLANG_WARN_COMMA = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 431 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 432 | CLANG_WARN_EMPTY_BODY = YES; 433 | CLANG_WARN_ENUM_CONVERSION = YES; 434 | CLANG_WARN_INFINITE_RECURSION = YES; 435 | CLANG_WARN_INT_CONVERSION = YES; 436 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 437 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 438 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 439 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 440 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 441 | CLANG_WARN_STRICT_PROTOTYPES = YES; 442 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 443 | CLANG_WARN_UNREACHABLE_CODE = YES; 444 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 445 | CODE_SIGN_IDENTITY = "iPhone Developer"; 446 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 447 | COPY_PHASE_STRIP = YES; 448 | CURRENT_PROJECT_VERSION = 1; 449 | ENABLE_NS_ASSERTIONS = NO; 450 | ENABLE_STRICT_OBJC_MSGSEND = YES; 451 | GCC_C_LANGUAGE_STANDARD = gnu99; 452 | GCC_NO_COMMON_BLOCKS = YES; 453 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 454 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 455 | GCC_WARN_UNDECLARED_SELECTOR = YES; 456 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 457 | GCC_WARN_UNUSED_FUNCTION = YES; 458 | GCC_WARN_UNUSED_VARIABLE = YES; 459 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 460 | MTL_ENABLE_DEBUG_INFO = NO; 461 | SDKROOT = iphoneos; 462 | TARGETED_DEVICE_FAMILY = "1,2"; 463 | VALIDATE_PRODUCT = YES; 464 | VERSIONING_SYSTEM = "apple-generic"; 465 | VERSION_INFO_PREFIX = ""; 466 | }; 467 | name = Release; 468 | }; 469 | 65B2CF2819EF9F6600DA3605 /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | CLANG_ENABLE_MODULES = YES; 473 | CODE_SIGN_IDENTITY = "iPhone Developer"; 474 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 475 | DEFINES_MODULE = YES; 476 | DYLIB_COMPATIBILITY_VERSION = 1; 477 | DYLIB_CURRENT_VERSION = 1; 478 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 479 | INFOPLIST_FILE = ImageLoader/Info.plist; 480 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 481 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 482 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 483 | PRODUCT_NAME = "$(TARGET_NAME)"; 484 | SKIP_INSTALL = YES; 485 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 486 | SWIFT_VERSION = 5.0; 487 | }; 488 | name = Debug; 489 | }; 490 | 65B2CF2919EF9F6600DA3605 /* Release */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | CLANG_ENABLE_MODULES = YES; 494 | CODE_SIGN_IDENTITY = "iPhone Developer"; 495 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 496 | DEFINES_MODULE = YES; 497 | DYLIB_COMPATIBILITY_VERSION = 1; 498 | DYLIB_CURRENT_VERSION = 1; 499 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 500 | INFOPLIST_FILE = ImageLoader/Info.plist; 501 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 502 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 503 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 504 | PRODUCT_NAME = "$(TARGET_NAME)"; 505 | SKIP_INSTALL = YES; 506 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 507 | SWIFT_VERSION = 5.0; 508 | }; 509 | name = Release; 510 | }; 511 | 65B2CF2B19EF9F6600DA3605 /* Debug */ = { 512 | isa = XCBuildConfiguration; 513 | buildSettings = { 514 | CLANG_ENABLE_MODULES = YES; 515 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 516 | GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; 517 | INFOPLIST_FILE = ImageLoaderTests/Info.plist; 518 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 519 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 522 | SWIFT_VERSION = 5.0; 523 | }; 524 | name = Debug; 525 | }; 526 | 65B2CF2C19EF9F6600DA3605 /* Release */ = { 527 | isa = XCBuildConfiguration; 528 | buildSettings = { 529 | CLANG_ENABLE_MODULES = YES; 530 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 531 | INFOPLIST_FILE = ImageLoaderTests/Info.plist; 532 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 533 | PRODUCT_BUNDLE_IDENTIFIER = "net.hirohisa.$(PRODUCT_NAME:rfc1034identifier)"; 534 | PRODUCT_NAME = "$(TARGET_NAME)"; 535 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 536 | SWIFT_VERSION = 5.0; 537 | }; 538 | name = Release; 539 | }; 540 | /* End XCBuildConfiguration section */ 541 | 542 | /* Begin XCConfigurationList section */ 543 | 65B2CF0B19EF9F6600DA3605 /* Build configuration list for PBXProject "ImageLoader" */ = { 544 | isa = XCConfigurationList; 545 | buildConfigurations = ( 546 | 65B2CF2519EF9F6600DA3605 /* Debug */, 547 | 65B2CF2619EF9F6600DA3605 /* Release */, 548 | ); 549 | defaultConfigurationIsVisible = 0; 550 | defaultConfigurationName = Release; 551 | }; 552 | 65B2CF2719EF9F6600DA3605 /* Build configuration list for PBXNativeTarget "ImageLoader" */ = { 553 | isa = XCConfigurationList; 554 | buildConfigurations = ( 555 | 65B2CF2819EF9F6600DA3605 /* Debug */, 556 | 65B2CF2919EF9F6600DA3605 /* Release */, 557 | ); 558 | defaultConfigurationIsVisible = 0; 559 | defaultConfigurationName = Release; 560 | }; 561 | 65B2CF2A19EF9F6600DA3605 /* Build configuration list for PBXNativeTarget "ImageLoaderTests" */ = { 562 | isa = XCConfigurationList; 563 | buildConfigurations = ( 564 | 65B2CF2B19EF9F6600DA3605 /* Debug */, 565 | 65B2CF2C19EF9F6600DA3605 /* Release */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | /* End XCConfigurationList section */ 571 | }; 572 | rootObject = 65B2CF0819EF9F6600DA3605 /* Project object */; 573 | } 574 | -------------------------------------------------------------------------------- /ImageLoader.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImageLoader.xcodeproj/xcshareddata/xcschemes/ImageLoader.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /ImageLoader.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ImageLoader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImageLoader.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ImageLoader/Core/Disk.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Disk.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 2016/11/07. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | public func escape() -> String? { 14 | return addingPercentEncoding(withAllowedCharacters: .alphanumerics) 15 | } 16 | } 17 | 18 | private let _ioQueue = DispatchQueue(label: "swift.imageloader.queues.disk.set") 19 | 20 | public struct Disk { 21 | 22 | let directory = Directory() 23 | let storage = HashStorage() 24 | 25 | public init() { 26 | } 27 | 28 | struct Directory { 29 | 30 | init() { 31 | createDirectory() 32 | } 33 | } 34 | } 35 | 36 | extension Disk.Directory { 37 | 38 | fileprivate func createDirectory() { 39 | let fileManager = FileManager.default 40 | if fileManager.fileExists(atPath: path) { 41 | return 42 | } 43 | 44 | do { 45 | try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 46 | } catch _ { 47 | } 48 | } 49 | 50 | var path: String { 51 | let cacheDirectory = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0] 52 | let directoryName = "swift.imageloader.disk" 53 | 54 | return cacheDirectory + "/" + directoryName 55 | } 56 | } 57 | 58 | 59 | extension Disk { 60 | 61 | public func cleanUp() { 62 | let manager = FileManager.default 63 | for subpath in manager.subpaths(atPath: directory.path) ?? [] { 64 | let path = directory.path + "/" + subpath 65 | do { 66 | try manager.removeItem(atPath: path) 67 | } catch _ { 68 | } 69 | } 70 | } 71 | 72 | public func get(_ aKey: String) -> Data? { 73 | if let data = storage[aKey] { 74 | return data 75 | } 76 | return (try? Data(contentsOf: URL(fileURLWithPath: _path(aKey)))) 77 | } 78 | 79 | public func get(_ aKey: URL) -> Data? { 80 | guard let key = aKey.absoluteString.escape() else { return nil } 81 | 82 | return get(key) 83 | } 84 | 85 | public func _path(_ name: String) -> String { 86 | return directory.path + "/" + name 87 | } 88 | 89 | public func set(_ anObject: Data, forKey aKey: String) { 90 | storage[aKey] = anObject 91 | 92 | let block: () -> Void = { 93 | do { 94 | try anObject.write(to: URL(fileURLWithPath: self._path(aKey)), options: []) 95 | self.storage[aKey] = nil 96 | } catch _ {} 97 | } 98 | 99 | _ioQueue.async(execute: block) 100 | } 101 | 102 | public func set(_ anObject: Data, forKey aKey: URL) { 103 | guard let key = aKey.absoluteString.escape() else { return } 104 | set(anObject, forKey: key) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ImageLoader/Core/HashStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HashStorage.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 11/7/16. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class HashStorage { 12 | private var items = [K: V]() 13 | public init() {} 14 | public subscript(key: K) -> V? { 15 | get { 16 | objc_sync_enter(HashStorage.self) 17 | defer { objc_sync_exit(HashStorage.self) } 18 | return items[key] 19 | } 20 | set(value) { 21 | objc_sync_enter(HashStorage.self) 22 | defer { objc_sync_exit(HashStorage.self) } 23 | items[key] = value 24 | } 25 | } 26 | 27 | func getKey(_ value: V) -> K? { 28 | var key: K? 29 | items.forEach { k, v in 30 | if v == value { 31 | key = k 32 | } 33 | } 34 | 35 | return key 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ImageLoader/Core/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 10/16/14. 6 | // Copyright © 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public struct ImageLoader { 13 | 14 | @discardableResult 15 | public static func request(with url: URLLiteralConvertible, onCompletion: @escaping (UIImage?, Error?, FetchOperation) -> Void) -> Loader? { 16 | guard let imageLoaderUrl = url.imageLoaderURL else { return nil } 17 | 18 | let task = Task(nil, onCompletion: onCompletion) 19 | let loader = ImageLoader.session.getLoader(with: imageLoaderUrl, task: task) 20 | loader.resume() 21 | 22 | return loader 23 | } 24 | 25 | static var session: ImageLoader.Session { 26 | return Session.shared 27 | } 28 | 29 | static var manager: ImageLoader.LoaderManager { 30 | return Session.manager 31 | } 32 | 33 | class Session: NSObject, URLSessionDataDelegate { 34 | 35 | static let shared = Session() 36 | static let manager = LoaderManager() 37 | 38 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 39 | guard let loader = getLoader(with: dataTask) else { return } 40 | loader.operative.receiveData.append(data) 41 | } 42 | 43 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 44 | completionHandler(.allow) 45 | } 46 | 47 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 48 | guard let loader = getLoader(with: task) else { return } 49 | loader.complete(with: error) 50 | } 51 | 52 | func getLoader(with dataTask: URLSessionTask) -> Loader? { 53 | guard let url = dataTask.originalRequest?.url else { return nil } 54 | return ImageLoader.manager.storage[url] 55 | } 56 | 57 | func getLoader(with url: URL, task: Task) -> Loader { 58 | let loader = ImageLoader.manager.getLoader(with: url) 59 | loader.operative.update(task) 60 | return loader 61 | } 62 | } 63 | 64 | class LoaderManager { 65 | 66 | let session: URLSession 67 | let storage = HashStorage() 68 | var disk = Disk() 69 | 70 | init(configuration: URLSessionConfiguration = .default) { 71 | self.session = URLSession(configuration: .default, delegate: ImageLoader.session, delegateQueue: nil) 72 | } 73 | 74 | func getLoader(with url: URL) -> Loader { 75 | if let loader = storage[url] { 76 | return loader 77 | } 78 | 79 | let loader = Loader(session.dataTask(with: url), url: url, delegate: self) 80 | storage[url] = loader 81 | return loader 82 | } 83 | 84 | func getLoader(with url: URL, task: Task) -> Loader { 85 | let loader = getLoader(with: url) 86 | loader.operative.update(task) 87 | return loader 88 | } 89 | 90 | func remove(_ loader: Loader) { 91 | guard let url = storage.getKey(loader) else { return } 92 | 93 | storage[url] = nil 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ImageLoader/Core/Loadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadable.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 2016/10/31. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public struct Loadable { 13 | 14 | let base: Base 15 | init(_ base: Base) { 16 | self.base = base 17 | } 18 | } 19 | 20 | public protocol LoadableCompatible { 21 | associatedtype CompatibleType 22 | 23 | var load: Loadable { get } 24 | } 25 | 26 | extension LoadableCompatible { 27 | 28 | public var load: Loadable { 29 | return Loadable(self) 30 | } 31 | } 32 | 33 | extension UIImageView: LoadableCompatible {} 34 | -------------------------------------------------------------------------------- /ImageLoader/Core/Loader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 10/29/16. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public struct Loader { 13 | 14 | unowned let delegate: ImageLoader.LoaderManager 15 | let task: URLSessionDataTask 16 | let operative = Operative() 17 | let url: URL 18 | 19 | init(_ task: URLSessionDataTask, url: URL, delegate: ImageLoader.LoaderManager) { 20 | self.task = task 21 | self.url = url 22 | self.delegate = delegate 23 | } 24 | 25 | public var state: URLSessionTask.State { 26 | return task.state 27 | } 28 | 29 | public func resume() { 30 | task.resume() 31 | } 32 | 33 | public func cancel() { 34 | task.cancel() 35 | 36 | let reason = "Cancel to request: \(url)" 37 | onFailure(with: NSError(domain: "Imageloader", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: reason])) 38 | } 39 | 40 | func complete(with error: Error?) { 41 | if let error = error { 42 | onFailure(with: error) 43 | return 44 | } 45 | 46 | if let image = UIImage.process(data: operative.receiveData) { 47 | onSuccess(with: image) 48 | delegate.disk.set(operative.receiveData, forKey: url) 49 | return 50 | } 51 | 52 | if let statusCode = (task.response as? HTTPURLResponse)?.statusCode, statusCode >= 200, statusCode < 400 { 53 | let reason = "Disconnect on downloading caused by HTTPStatusCode: \(statusCode)" 54 | onFailure(with: NSError(domain: "Imageloader", code: statusCode, userInfo: [NSLocalizedFailureReasonErrorKey: reason])) 55 | return 56 | } 57 | 58 | failOnConvertToImage() 59 | } 60 | 61 | private func failOnConvertToImage() { 62 | onFailure(with: NSError(domain: "Imageloader", code: -999, userInfo: [NSLocalizedFailureReasonErrorKey: "Failure when convert image"])) 63 | } 64 | 65 | private func onSuccess(with image: UIImage) { 66 | operative.tasks.forEach { task in 67 | task.onCompletion(image, nil, .network) 68 | } 69 | operative.tasks = [] 70 | delegate.remove(self) 71 | } 72 | 73 | private func onFailure(with error: Error) { 74 | operative.tasks.forEach { task in 75 | task.onCompletion(nil, error, .error) 76 | } 77 | operative.tasks = [] 78 | delegate.remove(self) 79 | } 80 | } 81 | 82 | extension Loader: Equatable {} 83 | 84 | public func ==(lhs: Loader, rhs: Loader) -> Bool { 85 | return lhs.task == rhs.task 86 | } 87 | -------------------------------------------------------------------------------- /ImageLoader/Core/Operative.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Operative.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 2016/10/30. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class Task { 13 | weak var owner: AnyObject? 14 | let onCompletion: (UIImage?, Error?, FetchOperation) -> Void 15 | 16 | init(_ owner: AnyObject?, onCompletion: @escaping (UIImage?, Error?, FetchOperation) -> Void) { 17 | self.owner = owner 18 | self.onCompletion = onCompletion 19 | } 20 | } 21 | 22 | class Operative { 23 | var tasks = [Task]() 24 | var receiveData = Data() 25 | 26 | func remove(_ task: Task) { 27 | guard let index = tasks.firstIndex(of: task) else { 28 | return 29 | } 30 | tasks.remove(at: index) 31 | } 32 | 33 | func update(_ task: Task) { 34 | guard let index = tasks.firstIndex(of: task) else { 35 | tasks.append(task) 36 | return 37 | } 38 | tasks[index] = task 39 | } 40 | } 41 | 42 | extension Task: Equatable {} 43 | 44 | func ==(lhs: Task, rhs: Task) -> Bool { 45 | guard let lOwner = lhs.owner, let rOwner = rhs.owner else { return false } 46 | 47 | return lOwner.isEqual(rOwner) 48 | } 49 | -------------------------------------------------------------------------------- /ImageLoader/Core/Option.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Option.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 11/16/16. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum FetchOperation { 12 | 13 | case disk 14 | case network 15 | case error 16 | 17 | } 18 | 19 | public enum Option { 20 | case adjustSize 21 | } 22 | -------------------------------------------------------------------------------- /ImageLoader/Core/URLLiteralConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLLiteralConvertible.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 11/4/16. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol URLLiteralConvertible { 12 | var imageLoaderURL: URL? { get } 13 | } 14 | 15 | extension URL: URLLiteralConvertible { 16 | public var imageLoaderURL: URL? { 17 | return self 18 | } 19 | } 20 | 21 | extension URLComponents: URLLiteralConvertible { 22 | public var imageLoaderURL: URL? { 23 | return url 24 | } 25 | } 26 | 27 | extension String: URLLiteralConvertible { 28 | public var imageLoaderURL: URL? { 29 | if let string = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { 30 | return URL(string: string) 31 | } 32 | return URL(string: self) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ImageLoader/Extension/CGImageSource+ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImageSource+ImageLoader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 4/17/18. 6 | // Copyright © 2018 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import ImageIO 12 | 13 | extension CGImageSource { 14 | 15 | internal var imageCount: Int { 16 | return CGImageSourceGetCount(self) 17 | } 18 | 19 | internal func process() -> (images: [UIImage], duration: TimeInterval) { 20 | var images = [UIImage]() 21 | let count = imageCount 22 | let duration: Double = Double(count) * 0.2 // TODO: implementation 23 | for i in 0 ..< count { 24 | if let cgImage = getCGImage(index: i) { 25 | images.append(UIImage(cgImage: cgImage)) 26 | } 27 | } 28 | 29 | return (images, duration) 30 | } 31 | 32 | internal func getCGImage(index: Int) -> CGImage? { 33 | return CGImageSourceCreateImageAtIndex(self, index, nil) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ImageLoader/Extension/Data+ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+ImageLoader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 4/17/18. 6 | // Copyright © 2018 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | 13 | enum FileType { 14 | case png 15 | case jpeg 16 | case gif 17 | case tiff 18 | case webp 19 | case Unknown 20 | } 21 | 22 | internal var fileType: FileType { 23 | let fileHeader = getFileHeader(capacity: 2) 24 | // https://en.wikipedia.org/wiki/List_of_file_signatures 25 | switch fileHeader { 26 | case [0x47, 0x49]: 27 | return .gif 28 | case [0xFF, 0xD8]: 29 | // FF D8 FF DB 30 | // FF D8 FF E0 31 | // FF D8 FF E1 32 | return .jpeg 33 | case [0x89, 0x50]: 34 | // 89 50 4E 47 35 | return .png 36 | default: 37 | return .Unknown 38 | } 39 | } 40 | 41 | internal func getFileHeader(capacity: Int) -> [UInt8] { 42 | 43 | // https://developer.apple.com/documentation/swift/unsafemutablepointer 44 | var pointer = UnsafeMutablePointer.allocate(capacity: capacity) 45 | // malloc: *** error for object 0x60c00001af32: pointer being freed was not allocated 46 | // defer { pointer.deallocate() } 47 | (self as NSData).getBytes(pointer, length: capacity) 48 | 49 | var header = [UInt8]() 50 | for _ in 0 ..< capacity { 51 | header.append(pointer.pointee) 52 | pointer += 1 53 | } 54 | 55 | return header 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ImageLoader/Extension/UIImage+ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+ImageLoader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 10/28/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import ImageIO 12 | 13 | private let lock = NSRecursiveLock() 14 | 15 | // MARK: Optimize image 16 | 17 | extension UIImage { 18 | 19 | func adjust(_ size: CGSize, scale: CGFloat, contentMode: UIView.ContentMode) -> UIImage { 20 | lock.lock() 21 | defer { lock.unlock() } 22 | 23 | if images?.count ?? 0 > 1 { 24 | return self 25 | } 26 | 27 | switch contentMode { 28 | case .scaleToFill: 29 | if size.width * scale > self.size.width || size.height * scale > self.size.height { 30 | return self 31 | } 32 | 33 | let fitSize = CGSize(width: size.width * scale, height: size.height * scale) 34 | return render(fitSize) 35 | case .scaleAspectFit: 36 | if size.width * scale > self.size.width || size.height * scale > self.size.height { 37 | return self 38 | } 39 | 40 | let downscaleSize = CGSize(width: self.size.width / scale, height: self.size.height / scale) 41 | let ratio = size.width/downscaleSize.width < size.height/downscaleSize.height ? size.width/downscaleSize.width : size.height/downscaleSize.height 42 | 43 | let fitSize = CGSize(width: downscaleSize.width * ratio * scale, height: downscaleSize.height * ratio * scale) 44 | return render(fitSize) 45 | case .scaleAspectFill: 46 | if size.width * scale > self.size.width || size.height * scale > self.size.height { 47 | return self 48 | } 49 | 50 | let downscaleSize = CGSize(width: self.size.width / scale, height: self.size.height / scale) 51 | let ratio = size.width/downscaleSize.width > size.height/downscaleSize.height ? size.width/downscaleSize.width : size.height/downscaleSize.height 52 | 53 | let fitSize = CGSize(width: downscaleSize.width * ratio * scale, height: downscaleSize.height * ratio * scale) 54 | return render(fitSize) 55 | default: 56 | return self 57 | } 58 | } 59 | 60 | func render(_ size: CGSize) -> UIImage { 61 | lock.lock() 62 | defer { lock.unlock() } 63 | 64 | if size.width == 0 || size.height == 0 { 65 | return self 66 | } 67 | 68 | UIGraphicsBeginImageContext(size) 69 | draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 70 | 71 | return UIGraphicsGetImageFromCurrentImageContext()! 72 | } 73 | 74 | internal static func process(data: Data) -> UIImage? { 75 | switch data.fileType { 76 | case .gif: 77 | guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil } 78 | let result = source.process() 79 | return UIImage.animatedImage(with: result.images, duration: result.duration) 80 | case .png, .jpeg, .tiff, .webp, .Unknown: 81 | return UIImage(data: data) 82 | } 83 | } 84 | 85 | static func decode(_ data: Data) -> UIImage? { 86 | lock.lock() 87 | defer { lock.unlock() } 88 | 89 | return UIImage(data: data) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /ImageLoader/Extension/UIImageView+ImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+ImageLoader.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 2016/10/31. 6 | // Copyright © 2016 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var ImageLoaderRequestUrlKey = 0 12 | private let _ioQueue = DispatchQueue(label: "swift.imageloader.queues.io", attributes: .concurrent) 13 | 14 | extension UIImageView { 15 | fileprivate var requestUrl: URL? { 16 | get { 17 | var requestUrl: URL? 18 | _ioQueue.sync { 19 | requestUrl = objc_getAssociatedObject(self, &ImageLoaderRequestUrlKey) as? URL 20 | } 21 | 22 | return requestUrl 23 | } 24 | set(newValue) { 25 | _ioQueue.async { 26 | objc_setAssociatedObject(self, &ImageLoaderRequestUrlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 27 | } 28 | } 29 | } 30 | } 31 | 32 | extension Loadable where Base: UIImageView { 33 | 34 | @discardableResult 35 | public func request(with url: URLLiteralConvertible, options: [Option] = []) -> Loader? { 36 | return request(with: url, placeholder: nil, options: options, onCompletion: { _,_,_ in }) 37 | } 38 | 39 | @discardableResult 40 | public func request(with url: URLLiteralConvertible, options: [Option] = [], onCompletion: @escaping (UIImage?, Error?, FetchOperation) -> Void) -> Loader? { 41 | return request(with: url, placeholder: nil, options: options, onCompletion: onCompletion) 42 | } 43 | 44 | @discardableResult 45 | public func request(with url: URLLiteralConvertible, placeholder: UIImage?, options: [Option] = [], onCompletion: @escaping (UIImage?, Error?, FetchOperation) -> Void) -> Loader? { 46 | guard let imageLoaderUrl = url.imageLoaderURL else { return nil } 47 | 48 | let imageCompletion: (UIImage?, Error?, FetchOperation) -> Void = { image, error, operation in 49 | guard var image = image else { return onCompletion(nil, error, operation) } 50 | 51 | DispatchQueue.main.async { 52 | if options.contains(.adjustSize) { 53 | image = image.adjust(self.base.frame.size, scale: UIScreen.main.scale, contentMode: self.base.contentMode) 54 | } 55 | if let images = image.images, images.count > 0, let gif = UIImage.animatedImage(with: images, duration: 1) { 56 | image = gif 57 | } 58 | self.base.image = image 59 | onCompletion(image, error, operation) 60 | } 61 | } 62 | 63 | let task = Task(base, onCompletion: imageCompletion) 64 | 65 | // cancel 66 | if let requestUrl = base.requestUrl { 67 | let loader = ImageLoader.manager.getLoader(with: requestUrl) 68 | loader.operative.remove(task) 69 | if requestUrl != imageLoaderUrl, loader.operative.tasks.isEmpty { 70 | loader.cancel() 71 | } 72 | } 73 | base.requestUrl = url.imageLoaderURL 74 | 75 | // disk 76 | base.image = placeholder ?? nil 77 | if let data = ImageLoader.manager.disk.get(imageLoaderUrl), let image = UIImage.process(data: data) { 78 | task.onCompletion(image, nil, .disk) 79 | return nil 80 | } 81 | 82 | // request 83 | let loader = ImageLoader.manager.getLoader(with: imageLoaderUrl, task: task) 84 | loader.resume() 85 | 86 | return loader 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ImageLoader/ImageLoader.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoader.h 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 10/16/14. 6 | // Copyright © 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | FOUNDATION_EXPORT double ImageLoaderVersionNumber; 12 | FOUNDATION_EXPORT const unsigned char ImageLoaderVersionString[]; 13 | -------------------------------------------------------------------------------- /ImageLoader/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ImageLoaderTests/DiskTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskTests.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 12/1/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImageLoader 11 | 12 | class DiskTests: ImageLoaderTestCase { 13 | 14 | func testSetAndGet() { 15 | let url = URL(string: "http://test/sample")! 16 | let data = generateData() 17 | 18 | let disk = Disk() 19 | disk.set(data, forKey: url) 20 | 21 | let actual = disk.get(url) 22 | 23 | XCTAssertNotNil(actual) 24 | XCTAssertEqual(actual, data) 25 | } 26 | 27 | func testSetAndGetWithString() { 28 | let key = "1234" 29 | 30 | let data = generateData() 31 | 32 | let disk = Disk() 33 | disk.set(data, forKey: key) 34 | 35 | XCTAssertNotNil(disk.get(key)) 36 | XCTAssertEqual(disk.get(key)!, data) 37 | } 38 | 39 | func testSetFromURLAndGetWithString() { 40 | let string = "http://test.com" 41 | let encodedString = "http%3A%2F%2Ftest%2Ecom" 42 | 43 | let url = URL(string: string)! 44 | let data = generateData() 45 | 46 | let disk = Disk() 47 | disk.set(data, forKey: url) 48 | 49 | sleep(1) 50 | 51 | XCTAssertNotNil(disk.get(encodedString)) 52 | XCTAssertEqual(disk.get(encodedString)!, data) 53 | } 54 | 55 | func testSetAndWriteToDisk() { 56 | let url = URL(string: "http://test/save_to_file")! 57 | let key = url.absoluteString.escape()! 58 | let data = generateData() 59 | 60 | let disk = Disk() 61 | disk.set(data, forKey: url) 62 | XCTAssertNotNil(disk.storage[key]) 63 | 64 | sleep(1) 65 | 66 | let actual = disk.get(url) 67 | 68 | XCTAssertNotNil(actual) 69 | XCTAssertEqual(actual, data) 70 | XCTAssertNil(disk.storage[key]) 71 | } 72 | 73 | func testCleanDisk() { 74 | let url = URL(string: "http://test/save_to_file_for_clean")! 75 | let data = generateData() 76 | 77 | let disk = Disk() 78 | disk.set(data, forKey: url) 79 | 80 | sleep(1) 81 | disk.cleanUp() 82 | sleep(1) 83 | 84 | XCTAssertNil(disk.get(url)) 85 | } 86 | 87 | func generateData() -> Data { 88 | let image = UIImage(color: UIColor.black, size: CGSize(width: 1, height: 1))! 89 | let data = image.jpegData(compressionQuality: 1)! 90 | 91 | return data 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /ImageLoaderTests/ImageLoaderTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoaderTestCase.swift 3 | // ImageLoaderTests 4 | // 5 | // Created by Hirohisa Kawasaki on 10/16/14. 6 | // Copyright © 2014 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import ImageLoader 12 | 13 | extension URLSessionTask.State { 14 | 15 | func toString() -> String { 16 | switch self { 17 | case .running: 18 | return "Running" 19 | case .suspended: 20 | return "Suspended" 21 | case .canceling: 22 | return "Canceling" 23 | case .completed: 24 | return "Completed" 25 | @unknown default: 26 | return "Unknown" 27 | } 28 | } 29 | } 30 | 31 | class ImageLoaderTestCase: XCTestCase { 32 | 33 | func stub() { 34 | URLSessionConfiguration.swizzleDefaultToMock() 35 | } 36 | 37 | func sleep(_ duration: TimeInterval = 0.01) { 38 | RunLoop.main.run(until: Date(timeIntervalSinceNow: duration)) 39 | } 40 | } 41 | 42 | extension URLSessionConfiguration { 43 | 44 | class func swizzleDefaultToMock() { 45 | let defaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.default)) 46 | let swizzledDefaultSessionConfiguration = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.mock)) 47 | method_exchangeImplementations(defaultSessionConfiguration!, swizzledDefaultSessionConfiguration!) 48 | } 49 | 50 | @objc private dynamic class var mock: URLSessionConfiguration { 51 | let configuration = self.mock 52 | configuration.protocolClasses?.insert(URLProtocolMock.self, at: 0) 53 | URLProtocol.registerClass(URLProtocolMock.self) 54 | return configuration 55 | } 56 | } 57 | 58 | public class URLProtocolMock: URLProtocol { 59 | 60 | override public class func canInit(with request:URLRequest) -> Bool { 61 | return true 62 | } 63 | 64 | override public class func canonicalRequest(for request: URLRequest) -> URLRequest { 65 | return request 66 | } 67 | 68 | override public func startLoading() { 69 | let delay: Double = 1.0 70 | DispatchQueue.global().asyncAfter(deadline: .now() + delay) { 71 | if let error = self.makeError() { 72 | self.client?.urlProtocol(self, didFailWithError: error) 73 | return 74 | } 75 | 76 | self.client?.urlProtocol(self, didLoad: self.makeResponse()) 77 | self.client?.urlProtocolDidFinishLoading(self) 78 | } 79 | } 80 | 81 | override public func stopLoading() {} 82 | 83 | private func makeResponse() -> Data { 84 | var data = Data() 85 | if let path = request.url?.path , !path.isEmpty { 86 | switch path { 87 | case _ where path.hasSuffix("white"): 88 | let imagePath = Bundle(for: type(of: self)).path(forResource: "white", ofType: "png")! 89 | data = UIImage(contentsOfFile: imagePath)!.pngData()! 90 | case _ where path.hasSuffix("black"): 91 | let imagePath = Bundle(for: type(of: self)).path(forResource: "black", ofType: "png")! 92 | data = UIImage(contentsOfFile: imagePath)!.pngData()! 93 | default: 94 | break 95 | } 96 | } 97 | 98 | return data 99 | } 100 | 101 | private func makeError() -> Error? { 102 | if let path = request.url?.path , !path.isEmpty { 103 | if let statusCode = Int(path) , 400 <= statusCode && statusCode < 600 { 104 | return NSError(domain: "Imageloader", code: statusCode, userInfo: [NSLocalizedFailureReasonErrorKey: "internet error with stub"]) 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /ImageLoaderTests/ImageLoaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageLoaderTests.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 12/1/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImageLoader 11 | 12 | class ImageLoaderTests: ImageLoaderTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | stub() 17 | } 18 | 19 | func testLoad() { 20 | let expectation = self.expectation(description: "wait until loader complete") 21 | 22 | let url = URL(string: "http://example/test/load")! 23 | let onCompletion: (UIImage?, Error?, FetchOperation) -> Void = { _,_,_ -> Void in 24 | expectation.fulfill() 25 | } 26 | 27 | let loader = ImageLoader.request(with: url, onCompletion: onCompletion) 28 | XCTAssert(loader!.state == .running) 29 | 30 | waitForExpectations(timeout: 5) { error in 31 | XCTAssertNil(error) 32 | } 33 | } 34 | 35 | func testCancel() { 36 | let expectation = self.expectation(description: "wait until loader complete") 37 | 38 | let url = URL(string: "http://example/test/cancel")! 39 | let onCompletion: (UIImage?, Error?, FetchOperation) -> Void = { _, error, _ -> Void in 40 | XCTAssertNotNil(error) 41 | expectation.fulfill() 42 | } 43 | 44 | let loader = ImageLoader.request(with: url, onCompletion: onCompletion) 45 | XCTAssert(loader!.state == .running) 46 | loader!.cancel() 47 | waitForExpectations(timeout: 5) { error in 48 | XCTAssertNil(error) 49 | } 50 | } 51 | 52 | func testLoadUrls() { 53 | let url1 = URL(string: "http://example/test/load/urls1")! 54 | let loader1 = ImageLoader.request(with: url1, onCompletion: { _,_,_ in }) 55 | 56 | let url2 = URL(string: "http://example/test/load/urls2")! 57 | let loader2 = ImageLoader.request(with: url2, onCompletion: { _,_,_ in }) 58 | 59 | XCTAssert(loader1!.state == .running) 60 | XCTAssert(loader2!.state == .running) 61 | XCTAssert(loader1 != loader2) 62 | } 63 | 64 | func testLoadSameUrl() { 65 | let url = URL(string: "http://example/test/load/same/url")! 66 | let loader1 = ImageLoader.request(with: url, onCompletion: { _,_,_ in }) 67 | let loader2 = ImageLoader.request(with: url, onCompletion: { _,_,_ in }) 68 | 69 | XCTAssert(loader1!.state == .running, loader1!.state.toString()) 70 | XCTAssert(loader2!.state == .running, loader2!.state.toString()) 71 | XCTAssert(loader1 == loader2) 72 | } 73 | 74 | func testLoadResponseCode404() { 75 | let expectation = self.expectation(description: "wait until loader complete") 76 | 77 | let url = URL(string: "http://example/404")! 78 | let _ = ImageLoader.request(with: url, onCompletion: { image, error, operation in 79 | XCTAssertNil(image) 80 | XCTAssertNotNil(error) 81 | expectation.fulfill() 82 | }) 83 | 84 | waitForExpectations(timeout: 5) { error in 85 | XCTAssertNil(error) 86 | } 87 | } 88 | 89 | func testCancelAfterLoading() { 90 | let url = URL(string: "http://example/test/cancel/after/loading")! 91 | let loader = ImageLoader.request(with: url, onCompletion: { _,_,_ in }) 92 | loader!.cancel() 93 | 94 | let actual = ImageLoader.manager.storage[url] 95 | XCTAssertNil(actual) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /ImageLoaderTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ImageLoaderTests/UIImageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageTests.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 10/15/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImageLoader 11 | 12 | extension UIImage { 13 | public convenience init?(color: UIColor) { 14 | self.init(color: color, size: CGSize(width: 1, height: 1)) 15 | } 16 | 17 | public convenience init?(color: UIColor, size: CGSize) { 18 | let frameFor1px = CGRect(x: 0, y: 0, width: size.width, height: size.height) 19 | UIGraphicsBeginImageContext(frameFor1px.size) 20 | let context = UIGraphicsGetCurrentContext() 21 | context?.setFillColor(color.cgColor) 22 | context?.fill(frameFor1px) 23 | 24 | let image = UIGraphicsGetImageFromCurrentImageContext() 25 | UIGraphicsEndImageContext() 26 | 27 | guard let cgImage = image?.cgImage else { return nil } 28 | 29 | self.init(cgImage: cgImage) 30 | } 31 | } 32 | 33 | class UIImageTests: XCTestCase { 34 | 35 | // MARK: - adjust 36 | func testImageAdjustScale() { 37 | var image: UIImage! 38 | var size: CGSize! 39 | var adjustedSize: CGSize! 40 | var adjustedImage: UIImage! 41 | 42 | image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 100)) 43 | 44 | size = CGSize(width: 50, height: 50) 45 | adjustedSize = CGSize(width: 50, height: 50) 46 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFit) 47 | XCTAssertEqual(adjustedSize, adjustedImage.size) 48 | 49 | size = CGSize(width: 50, height: 50) 50 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleAspectFit) 51 | adjustedSize = CGSize(width: 100, height: 100) 52 | XCTAssertEqual(adjustedSize, adjustedImage.size) 53 | 54 | size = CGSize(width: 200, height: 200) 55 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFit) 56 | adjustedSize = CGSize(width: 100, height: 100) 57 | XCTAssertEqual(adjustedSize, adjustedImage.size) 58 | } 59 | 60 | func testImageAdjustsRectangleScaleAspectFit1() { 61 | var image: UIImage! 62 | var size: CGSize! 63 | var adjustedSize: CGSize! 64 | var adjustedImage: UIImage! 65 | 66 | image = UIImage(color: UIColor.black, size: CGSize(width: 50, height: 30)) 67 | 68 | size = CGSize(width: 50, height: 50) 69 | adjustedSize = CGSize(width: 50, height: 30) 70 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFit) 71 | XCTAssertEqual(adjustedSize, adjustedImage.size) 72 | 73 | size = CGSize(width: 50, height: 60) 74 | adjustedSize = CGSize(width: 50, height: 30) 75 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFit) 76 | XCTAssertEqual(adjustedSize, adjustedImage.size) 77 | 78 | size = CGSize(width: 60, height: 30) 79 | adjustedSize = CGSize(width: 50, height: 30) 80 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFit) 81 | XCTAssertEqual(adjustedSize, adjustedImage.size) 82 | } 83 | 84 | func testImageAdjustsRectangleScaleAspectFit2() { 85 | var image: UIImage! 86 | var size: CGSize! 87 | var adjustedSize: CGSize! 88 | var adjustedImage: UIImage! 89 | 90 | image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 60)) 91 | 92 | size = CGSize(width: 25, height: 25) 93 | adjustedSize = CGSize(width: 50, height: 30) 94 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleAspectFit) 95 | XCTAssertEqual(adjustedSize, adjustedImage.size) 96 | 97 | size = CGSize(width: 25, height: 30) 98 | adjustedSize = CGSize(width: 50, height: 30) 99 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleAspectFit) 100 | XCTAssertEqual(adjustedSize, adjustedImage.size) 101 | 102 | size = CGSize(width: 30, height: 15) 103 | adjustedSize = CGSize(width: 50, height: 30) 104 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleAspectFit) 105 | XCTAssertEqual(adjustedSize, adjustedImage.size) 106 | } 107 | 108 | func testImageAdjustsRectangleScaleAspectFill1() { 109 | var image: UIImage! 110 | var size: CGSize! 111 | var adjustedSize: CGSize! 112 | var adjustedImage: UIImage! 113 | 114 | image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 80)) 115 | 116 | size = CGSize(width: 40, height: 40) 117 | adjustedSize = CGSize(width: 50, height: 40) 118 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFill) 119 | XCTAssertEqual(adjustedSize, adjustedImage.size) 120 | 121 | size = CGSize(width: 50, height: 80) 122 | adjustedSize = CGSize(width: 100, height: 80) 123 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFill) 124 | XCTAssertEqual(adjustedSize, adjustedImage.size) 125 | 126 | size = CGSize(width: 80, height: 40) 127 | adjustedSize = CGSize(width: 80, height: 64) 128 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleAspectFill) 129 | XCTAssertEqual(adjustedSize, adjustedImage.size) 130 | } 131 | 132 | func testImageAdjustsRectangleScaleToFill() { 133 | var image: UIImage! 134 | var size: CGSize! 135 | var adjustedSize: CGSize! 136 | var adjustedImage: UIImage! 137 | 138 | image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 80)) 139 | 140 | // Scale 1 141 | 142 | size = CGSize(width: 40, height: 40) 143 | adjustedSize = CGSize(width: 40, height: 40) 144 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleToFill) 145 | XCTAssertEqual(adjustedSize, adjustedImage.size) 146 | 147 | size = CGSize(width: 50, height: 80) 148 | adjustedSize = CGSize(width: 50, height: 80) 149 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .scaleToFill) 150 | XCTAssertEqual(adjustedSize, adjustedImage.size) 151 | 152 | // Scale 2 153 | size = CGSize(width: 40, height: 40) 154 | adjustedSize = CGSize(width: 80, height: 80) 155 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleToFill) 156 | XCTAssertEqual(adjustedSize, adjustedImage.size) 157 | 158 | size = CGSize(width: 50, height: 80) 159 | adjustedSize = CGSize(width: 100, height: 80) 160 | adjustedImage = image!.adjust(size, scale: 2, contentMode: .scaleToFill) 161 | XCTAssertEqual(adjustedSize, adjustedImage.size) 162 | } 163 | 164 | func testImageAdjustsScaleUnsupportContentMode() { 165 | var image: UIImage! 166 | var size: CGSize! 167 | var adjustedSize: CGSize! 168 | var adjustedImage: UIImage! 169 | 170 | image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 100)) 171 | 172 | size = CGSize(width: 50, height: 50) 173 | adjustedSize = CGSize(width: 100, height: 100) 174 | 175 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .redraw) 176 | XCTAssertEqual(adjustedSize, adjustedImage.size) 177 | 178 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .center) 179 | XCTAssertEqual(adjustedSize, adjustedImage.size) 180 | 181 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .top) 182 | XCTAssertEqual(adjustedSize, adjustedImage.size) 183 | 184 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .left) 185 | XCTAssertEqual(adjustedSize, adjustedImage.size) 186 | 187 | adjustedImage = image!.adjust(size, scale: 1, contentMode: .right) 188 | XCTAssertEqual(adjustedSize, adjustedImage.size) 189 | } 190 | 191 | func testSizeIsZeroWhenRendering() { 192 | let image = UIImage(color: UIColor.black, size: CGSize(width: 100, height: 100))! 193 | let renderedImage = image.render(CGSize.zero) 194 | 195 | XCTAssertEqual(image, renderedImage) 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /ImageLoaderTests/UIImageViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageViewTests.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 12/2/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImageLoader 11 | 12 | // extension UIImage: Equatable 13 | 14 | extension UIImage { 15 | 16 | func isEqualTo(_ image: UIImage) -> Bool { 17 | if size == image.size { 18 | if let lcfdt = cgImage?.dataProvider?.data, let rcfdt = image.cgImage?.dataProvider?.data { 19 | let ldt = NSData(data: lcfdt as Data) 20 | let rdt = NSData(data: rcfdt as Data) 21 | return ldt == rdt 22 | } 23 | } 24 | 25 | return false 26 | } 27 | 28 | } 29 | 30 | class UIImageViewTests: ImageLoaderTestCase { 31 | 32 | let whiteImage: UIImage = { 33 | let imagePath = Bundle(for: UIImageViewTests.self).path(forResource: "white", ofType: "png")! 34 | return UIImage(contentsOfFile: imagePath)! 35 | }() 36 | 37 | let blackImage: UIImage = { 38 | let imagePath = Bundle(for: UIImageViewTests.self).path(forResource: "black", ofType: "png")! 39 | return UIImage(contentsOfFile: imagePath)! 40 | }() 41 | 42 | var imageView: UIImageView! 43 | 44 | override func setUp() { 45 | super.setUp() 46 | Disk().cleanUp() 47 | stub() 48 | imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200)) 49 | } 50 | 51 | func testLoadImage() { 52 | let expectation = self.expectation(description: "wait until loading") 53 | 54 | let string = "http://testLoadImage/white" 55 | 56 | imageView.load.request(with: string, onCompletion: { image, error, operation in 57 | XCTAssertNil(error) 58 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 59 | expectation.fulfill() 60 | }) 61 | 62 | waitForExpectations(timeout: 3) { error in 63 | XCTAssertNil(error) 64 | } 65 | } 66 | 67 | func testLoadImageUseCache() { 68 | let expectation = self.expectation(description: "wait until loading") 69 | 70 | let string = "http://testLoadImageUseCache/white" 71 | 72 | imageView.image = blackImage 73 | let loader1 = imageView.load.request(with: string) 74 | XCTAssertNotNil(loader1) 75 | 76 | sleep(3) 77 | 78 | XCTAssertTrue(imageView.image!.isEqualTo(self.whiteImage)) 79 | 80 | imageView.image = blackImage 81 | let loader2 = imageView.load.request(with: string, onCompletion: { _, _, operation in 82 | XCTAssertEqual(operation, .disk) 83 | expectation.fulfill() 84 | }) 85 | XCTAssertNil(loader2) 86 | 87 | sleep(1) 88 | 89 | XCTAssertTrue(imageView.image!.isEqualTo(self.whiteImage)) 90 | 91 | waitForExpectations(timeout: 3) { error in 92 | XCTAssertNil(error) 93 | } 94 | } 95 | 96 | func testLoadImageWithPlaceholder() { 97 | let expectation = self.expectation(description: "wait until loading") 98 | 99 | let string = "http://testLoadImageWithPlaceholder/white" 100 | 101 | imageView.image = blackImage 102 | imageView.load.request(with: string, onCompletion: { image, error, operation in 103 | XCTAssertNil(error) 104 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 105 | expectation.fulfill() 106 | }) 107 | XCTAssertTrue(imageView.image!.isEqualTo(self.blackImage)) 108 | 109 | waitForExpectations(timeout: 3) { error in 110 | XCTAssertNil(error) 111 | } 112 | } 113 | 114 | func testSetImageSoonAfterLoading() { 115 | let expectation = self.expectation(description: "wait until loading") 116 | 117 | let string = "http://testSetImageSoonAfterLoading/white" 118 | 119 | imageView.load.request(with: string, onCompletion: { image, error, operation in 120 | XCTAssertNil(error) 121 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 122 | expectation.fulfill() 123 | }) 124 | imageView.image = blackImage 125 | XCTAssertTrue(imageView.image!.isEqualTo(self.blackImage)) 126 | 127 | waitForExpectations(timeout: 3) { error in 128 | XCTAssertNil(error) 129 | } 130 | } 131 | 132 | func testLastestLoadIsAliveWhenTwiceLoad() { 133 | let expectation = self.expectation(description: "wait until loading") 134 | 135 | let string1 = "http://testLastestLoadIsAliveWhenTwiceLoad/black" 136 | let string2 = "http://testLastestLoadIsAliveWhenTwiceLoad/white" 137 | 138 | imageView.load.request(with: string1, onCompletion: { image, error, operation in 139 | XCTAssertNotNil(error) 140 | }) 141 | imageView.load.request(with: string2, onCompletion: { image, error, operation in 142 | XCTAssertNil(error) 143 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 144 | expectation.fulfill() 145 | }) 146 | 147 | waitForExpectations(timeout: 3) { error in 148 | XCTAssertNil(error) 149 | } 150 | } 151 | 152 | func testLastestLoadIsAliveWhenTwiceLoadWithSameUrl() { 153 | let expectation = self.expectation(description: "wait until loading") 154 | 155 | let string = "http://testLastestLoadIsAliveWhenTwiceLoadWithSameUrl/white" 156 | 157 | let loader1 = imageView.load.request(with: string, onCompletion: { _,_,_ in 158 | XCTFail() 159 | }) 160 | XCTAssertEqual(loader1?.operative.tasks.count, 1) 161 | let loader2 = imageView.load.request(with: string, onCompletion: { image, error, operation in 162 | XCTAssertNil(error) 163 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 164 | expectation.fulfill() 165 | }) 166 | XCTAssertEqual(loader2?.operative.tasks.count, 1) 167 | 168 | XCTAssertEqual(loader1, loader2) 169 | 170 | waitForExpectations(timeout: 3) { error in 171 | XCTAssertNil(error) 172 | } 173 | } 174 | 175 | func testTwiceLoadsInLoadingCompletion() { 176 | let expectation = self.expectation(description: "wait until loading") 177 | 178 | let string = "http://testTwiceLoadsInLoadingCompletion/1/white" 179 | let string1 = "http://testTwiceLoadsInLoadingCompletion/2/black" 180 | let string2 = "http://testTwiceLoadsInLoadingCompletion/1/white" 181 | 182 | imageView.load.request(with: string, onCompletion: { image, error, operation in 183 | XCTAssertNil(error) 184 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 185 | XCTAssertTrue(self.imageView.image!.isEqualTo(self.whiteImage)) 186 | 187 | self.imageView.load.request(with: string1, onCompletion: { error,_,_ in 188 | XCTAssertNotNil(error) 189 | }) 190 | self.imageView.load.request(with: string2, onCompletion: { image, error, operation in 191 | XCTAssertTrue(image!.isEqualTo(self.whiteImage)) 192 | XCTAssertTrue(self.imageView.image!.isEqualTo(self.whiteImage)) 193 | expectation.fulfill() 194 | }) 195 | }) 196 | 197 | waitForExpectations(timeout: 6) { error in 198 | XCTAssertNil(error) 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /ImageLoaderTests/URLLiteralConvertibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLLiteralConvertibleTests.swift 3 | // ImageLoader 4 | // 5 | // Created by Hirohisa Kawasaki on 11/30/15. 6 | // Copyright © 2015 Hirohisa Kawasaki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class StringTests: XCTestCase { 12 | 13 | func testEscape() { 14 | let string = "http://test.com" 15 | let valid = "http%3A%2F%2Ftest%2Ecom" 16 | 17 | XCTAssertNotEqual(string, string.escape()) 18 | XCTAssertEqual(valid, string.escape()) 19 | } 20 | } 21 | 22 | class URLLiteralConvertibleTests: XCTestCase { 23 | 24 | func testEscapes() { 25 | let url = "http://twitter.com/?status=Hello World".imageLoaderURL 26 | let valid = URL(string: "http://twitter.com/?status=Hello%20World")! 27 | 28 | XCTAssertEqual(url, valid) 29 | } 30 | 31 | func testConvertImageLoaderURL() { 32 | let string = "https://host/path" 33 | let urlString = string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 34 | let url = URL(string: urlString)! 35 | let components = URLComponents(string: urlString)! 36 | 37 | XCTAssertEqual(string.imageLoaderURL, url) 38 | XCTAssertEqual(string.imageLoaderURL!.absoluteString, urlString) 39 | 40 | XCTAssertEqual(url.imageLoaderURL, url) 41 | XCTAssertEqual(url.imageLoaderURL!.absoluteString, urlString) 42 | 43 | XCTAssertEqual(components.imageLoaderURL, url) 44 | XCTAssertEqual(components.imageLoaderURL!.absoluteString, urlString) 45 | } 46 | 47 | func testConvertImageLoaderURLIfNeededPercentEncoding() { 48 | let string = "https://host/path?query=1枚目" 49 | let urlString = string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 50 | let url = URL(string: urlString)! 51 | let components = URLComponents(string: urlString)! 52 | 53 | XCTAssertEqual(string.imageLoaderURL, url) 54 | XCTAssertEqual(string.imageLoaderURL!.absoluteString, urlString) 55 | 56 | XCTAssertEqual(url.imageLoaderURL, url) 57 | XCTAssertEqual(url.imageLoaderURL!.absoluteString, urlString) 58 | 59 | XCTAssertEqual(components.imageLoaderURL, url) 60 | XCTAssertEqual(components.imageLoaderURL!.absoluteString, urlString) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ImageLoaderTests/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirohisa/ImageLoaderSwift/f86e15e14f7f6088e9a2171821e60bf4f701a0d7/ImageLoaderTests/black.png -------------------------------------------------------------------------------- /ImageLoaderTests/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirohisa/ImageLoaderSwift/f86e15e14f7f6088e9a2171821e60bf4f701a0d7/ImageLoaderTests/white.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Hirohisa Kawasaki 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ImageLoader 2 | ======= 3 | [![Build-Status](https://api.travis-ci.org/hirohisa/ImageLoaderSwift.svg?branch=master)](https://travis-ci.org/hirohisa/ImageLoaderSwift) 4 | [![CocoaPods](https://img.shields.io/cocoapods/v/ImageLoader.svg)](https://cocoapods.org/pods/ImageLoader) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![codecov.io](https://codecov.io/github/hirohisa/ImageLoaderSwift/coverage.svg?branch=master)](https://codecov.io/github/hirohisa/ImageLoaderSwift?branch=master) 7 | [![license](https://img.shields.io/badge/license-MIT-000000.svg)](https://github.com/hirohisa/ImageLoaderSwift/blob/master/LICENSE) 8 | 9 | ImageLoader is an instrument for asynchronous image loading written in Swift. It is a lightweight and fast image loader for iOS. 10 | 11 | Features 12 | ---------- 13 | 14 | - [x] Simple methods with UIImageView Category. 15 | - [x] Control Loader to resume, suspend and cancel with URL. 16 | - [x] A module for cache can be set by yourself and default cache (Disk) uses disk spaces and un-uses memory. 17 | - [x] Loading images is handled by ImageLoader, not UIImageView. 18 | - [x] After image view start loading another image, previous loading task is possible to live with caching. 19 | - [x] Support `NSURL`, `String` and `NSURLComponents` by `URLLiteralConvertible` 20 | - [ ] Optimize to use memory when image is set. 21 | - [x] Support image type .jpeg, .png 22 | - [x] Comprehensive Unit Test Coverage 23 | 24 | Requirements 25 | ---------- 26 | 27 | - iOS 8.0+ 28 | - Xcode 7.0+ Swift 2.0 29 | 30 | ImageLoader | Xcode | Swift 31 | ----------- | ----- | ----- 32 | 0.13.+ | 9.0+ | 4.0 33 | 0.12.+ | 8.1+ | 3.0 34 | 0.11.+ | 8.0+ | 3.0 35 | 0.10.0 | 8.0+ | 2.3 36 | 0.9.x | 7.3.1 | 2.2 37 | 38 | If your project's target need to support iOS5.x or 6.x, use [ImageLoader](https://github.com/hirohisa/ImageLoader). It's A lightweight and fast image loader for iOS written in Objective-C. 39 | 40 | Installation 41 | ---------- 42 | 43 | ### CocoaPods 44 | 45 | ```ruby 46 | pod 'ImageLoader' 47 | ``` 48 | 49 | ### Carthage 50 | 51 | To integrate ImageLoader into your Xcode project using Carthage, specify it in your `Cartfile`: 52 | 53 | ``` 54 | github "hirohisa/ImageLoaderSwift" ~> 0.6.0 55 | ``` 56 | 57 | Usage 58 | ---------- 59 | 60 | #### ImageLoader 61 | 62 | **load** 63 | ```swift 64 | ImageLoader.request(with: url, onCompletion: { _ in }) 65 | ``` 66 | 67 | #### UIImageView 68 | 69 | ```swift 70 | imageView.load.request(with: url) 71 | ``` 72 | 73 | or 74 | 75 | ```swift 76 | imageView.load.request(with: url, onCompletion: { _ in }) 77 | ``` 78 | 79 | 80 | ## License 81 | 82 | ImageLoader is available under the MIT license. 83 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | NAME = "ImageLoader" 2 | WORKSPACE = "#{NAME}.xcworkspace" 3 | 4 | def destination 5 | list = [] 6 | `instruments -s devices`.each_line {|str| 7 | regx = 'iPhone.* \((.*)\) \[(.*)\].*(Simulator)' 8 | if match = str.match(/#{regx}/) 9 | list << { 10 | string: str.chomp, 11 | OS: $1, 12 | id: $2, 13 | } 14 | end 15 | } 16 | 17 | "platform=iOS Simulator,id=#{list.last[:id]}" 18 | end 19 | 20 | task :test do 21 | sh "xcodebuild test -workspace #{WORKSPACE} -scheme #{NAME} -destination \"#{destination()}\" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES test | xcpretty" 22 | end 23 | --------------------------------------------------------------------------------