├── .gitignore ├── .swift-version ├── .travis.yml ├── Assets └── github-header.png ├── Cartfile.private ├── Cartfile.resolved ├── Haneke.playground ├── contents.xcplayground ├── playground.xcworkspace │ └── contents.xcworkspacedata └── section-1.swift ├── Haneke.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── Haneke-iOS.xcscheme │ ├── Haneke-tvOS.xcscheme │ └── HanekeDemo.xcscheme ├── Haneke.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── Haneke.xcscmblueprint │ └── IDEWorkspaceChecks.plist ├── Haneke ├── CGSize+Swift.swift ├── Cache.swift ├── CryptoSwiftMD5.swift ├── Data.swift ├── DiskCache.swift ├── DiskFetcher.swift ├── Fetch.swift ├── Fetcher.swift ├── Format.swift ├── Haneke.h ├── Haneke.swift ├── Info-iOS.plist ├── Info-tvOS.plist ├── Log.swift ├── NSFileManager+Haneke.swift ├── NSHTTPURLResponse+Haneke.swift ├── NSURLResponse+Haneke.swift ├── NetworkFetcher.swift ├── String+Haneke.swift ├── UIButton+Haneke.swift ├── UIImage+Haneke.swift ├── UIImageView+Haneke.swift └── UIView+Haneke.swift ├── HanekeDemo ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── CollectionViewCell.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76.png │ │ └── icon-76@2x.png ├── Info.plist └── ViewController.swift ├── HanekeSwift.podspec ├── HanekeTests ├── AsyncFetcher.swift ├── CGSize+HanekeTests.swift ├── CacheTests.swift ├── DataTests.swift ├── DiskCacheTests.swift ├── DiskFetcherTests.swift ├── DiskTestCase.swift ├── FetchTests.swift ├── FetcherTests.swift ├── FormatTests.swift ├── HanekeTests-Bridging-Header.h ├── HanekeTests.swift ├── Info.plist ├── NSData+Test.swift ├── NSFileManager+HanekeTests.swift ├── NSHTTPURLResponse+HanekeTests.swift ├── NSURLResponse+HanekeTests.swift ├── NetworkFetcherTests.swift ├── String+HanekeTests.swift ├── UIButton+HanekeTests.swift ├── UIImage+HanekeTests.swift ├── UIImage+Test.swift ├── UIImageView+HanekeTests.swift └── XCTestCase+Test.swift ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods 22 | 23 | #Carthage 24 | Carthage 25 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10.2 3 | before_install: 4 | - brew update 5 | - brew install carthage || brew outdated carthage || brew upgrade carthage 6 | install: 7 | - carthage bootstrap 8 | branches: 9 | only: 10 | - master 11 | script: 12 | - set -o pipefail && xcodebuild build test -workspace Haneke.xcworkspace -scheme Haneke-iOS -destination 'platform=iOS Simulator,name=iPhone X,OS=12.0' | xcpretty --color 13 | -------------------------------------------------------------------------------- /Assets/github-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haneke/HanekeSwift/a2e8e5b9a91eef90138a4f43c9a0044c4e90a6ef/Assets/github-header.png -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "AliSoftware/OHHTTPStubs" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "AliSoftware/OHHTTPStubs" "7.0.0" 2 | -------------------------------------------------------------------------------- /Haneke.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 2 | <playground version='3.0' sdk='iphonesimulator' auto-termination-delay='100'> 3 | <sections> 4 | <code source-file-name='section-1.swift'/> 5 | </sections> 6 | <timeline fileName='timeline.xctimeline'/> 7 | </playground> -------------------------------------------------------------------------------- /Haneke.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Workspace 3 | version = "1.0"> 4 | <FileRef 5 | location = "self:"> 6 | </FileRef> 7 | </Workspace> 8 | -------------------------------------------------------------------------------- /Haneke.playground/section-1.swift: -------------------------------------------------------------------------------- 1 | // Open this playground from the Haneke workspace after building the Haneke framework. See: http://stackoverflow.com/a/24049021/143378 2 | import Haneke 3 | 4 | /// Initialize a JSON cache and fetch/cache a JSON response. 5 | func example1() { 6 | let cache = Cache<JSON>(name: "github") 7 | let url = URL(string: "https://api.github.com/users/haneke")! 8 | 9 | cache.fetch(URL: url).onSuccess { json in 10 | let bio = json.dictionary?["bio"] 11 | print(bio.map { String(describing: $0) } ?? "nil") 12 | } 13 | } 14 | 15 | /// Set a image view image from a url using the shared image cache and resizing. 16 | func example2() { 17 | let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) 18 | let url = URL(string: "https://avatars.githubusercontent.com/u/8600207?v=2")! 19 | 20 | imageView.hnk_setImageFromURL(url) 21 | } 22 | 23 | /// Set and fetch data from the shared data cache 24 | func example3() { 25 | let cache = Shared.dataCache 26 | let data = "SGVscCEgSSdtIHRyYXBwZWQgaW4gYSBCYXNlNjQgc3RyaW5nIQ==".asData()! 27 | 28 | cache.set(value: data, key: "secret") 29 | 30 | cache.fetch(key: "secret").onSuccess { fetchedData in 31 | let fetchedString = String(data: fetchedData, encoding: .utf8) 32 | print(fetchedString ?? "nil") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Haneke.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Workspace 3 | version = "1.0"> 4 | <FileRef 5 | location = "self:Haneke.xcodeproj"> 6 | </FileRef> 7 | </Workspace> 8 | -------------------------------------------------------------------------------- /Haneke.xcodeproj/xcshareddata/xcschemes/Haneke-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1020" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "YES" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "A095C9551980418C00CD0F4C" 18 | BuildableName = "Haneke.framework" 19 | BlueprintName = "Haneke-iOS" 20 | ReferencedContainer = "container:Haneke.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | <BuildActionEntry 24 | buildForTesting = "YES" 25 | buildForRunning = "NO" 26 | buildForProfiling = "NO" 27 | buildForArchiving = "NO" 28 | buildForAnalyzing = "NO"> 29 | <BuildableReference 30 | BuildableIdentifier = "primary" 31 | BlueprintIdentifier = "A095C9601980418C00CD0F4C" 32 | BuildableName = "HanekeTests.xctest" 33 | BlueprintName = "HanekeTests" 34 | ReferencedContainer = "container:Haneke.xcodeproj"> 35 | </BuildableReference> 36 | </BuildActionEntry> 37 | </BuildActionEntries> 38 | </BuildAction> 39 | <TestAction 40 | buildConfiguration = "Debug" 41 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 42 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 43 | codeCoverageEnabled = "YES" 44 | shouldUseLaunchSchemeArgsEnv = "YES"> 45 | <Testables> 46 | <TestableReference 47 | skipped = "NO"> 48 | <BuildableReference 49 | BuildableIdentifier = "primary" 50 | BlueprintIdentifier = "A095C9601980418C00CD0F4C" 51 | BuildableName = "HanekeTests.xctest" 52 | BlueprintName = "HanekeTests" 53 | ReferencedContainer = "container:Haneke.xcodeproj"> 54 | </BuildableReference> 55 | </TestableReference> 56 | </Testables> 57 | <MacroExpansion> 58 | <BuildableReference 59 | BuildableIdentifier = "primary" 60 | BlueprintIdentifier = "A095C9551980418C00CD0F4C" 61 | BuildableName = "Haneke.framework" 62 | BlueprintName = "Haneke-iOS" 63 | ReferencedContainer = "container:Haneke.xcodeproj"> 64 | </BuildableReference> 65 | </MacroExpansion> 66 | <AdditionalOptions> 67 | </AdditionalOptions> 68 | </TestAction> 69 | <LaunchAction 70 | buildConfiguration = "Debug" 71 | selectedDebuggerIdentifier = "" 72 | selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" 73 | launchStyle = "0" 74 | useCustomWorkingDirectory = "NO" 75 | ignoresPersistentStateOnLaunch = "NO" 76 | debugDocumentVersioning = "YES" 77 | debugServiceExtension = "internal" 78 | allowLocationSimulation = "YES"> 79 | <MacroExpansion> 80 | <BuildableReference 81 | BuildableIdentifier = "primary" 82 | BlueprintIdentifier = "A095C9551980418C00CD0F4C" 83 | BuildableName = "Haneke.framework" 84 | BlueprintName = "Haneke-iOS" 85 | ReferencedContainer = "container:Haneke.xcodeproj"> 86 | </BuildableReference> 87 | </MacroExpansion> 88 | <AdditionalOptions> 89 | </AdditionalOptions> 90 | </LaunchAction> 91 | <ProfileAction 92 | buildConfiguration = "Release" 93 | shouldUseLaunchSchemeArgsEnv = "YES" 94 | savedToolIdentifier = "" 95 | useCustomWorkingDirectory = "NO" 96 | debugDocumentVersioning = "YES"> 97 | <MacroExpansion> 98 | <BuildableReference 99 | BuildableIdentifier = "primary" 100 | BlueprintIdentifier = "A095C9551980418C00CD0F4C" 101 | BuildableName = "Haneke.framework" 102 | BlueprintName = "Haneke-iOS" 103 | ReferencedContainer = "container:Haneke.xcodeproj"> 104 | </BuildableReference> 105 | </MacroExpansion> 106 | </ProfileAction> 107 | <AnalyzeAction 108 | buildConfiguration = "Debug"> 109 | </AnalyzeAction> 110 | <ArchiveAction 111 | buildConfiguration = "Release" 112 | revealArchiveInOrganizer = "YES"> 113 | </ArchiveAction> 114 | </Scheme> 115 | -------------------------------------------------------------------------------- /Haneke.xcodeproj/xcshareddata/xcschemes/Haneke-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1020" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "YES" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "6393C5DA1C3B229200EB1FD8" 18 | BuildableName = "Haneke.framework" 19 | BlueprintName = "Haneke-tvOS" 20 | ReferencedContainer = "container:Haneke.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | </BuildActionEntries> 24 | </BuildAction> 25 | <TestAction 26 | buildConfiguration = "Debug" 27 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 28 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 29 | shouldUseLaunchSchemeArgsEnv = "YES"> 30 | <Testables> 31 | <TestableReference 32 | skipped = "NO"> 33 | <BuildableReference 34 | BuildableIdentifier = "primary" 35 | BlueprintIdentifier = "A095C9601980418C00CD0F4C" 36 | BuildableName = "HanekeTests.xctest" 37 | BlueprintName = "HanekeTests" 38 | ReferencedContainer = "container:Haneke.xcodeproj"> 39 | </BuildableReference> 40 | </TestableReference> 41 | </Testables> 42 | <MacroExpansion> 43 | <BuildableReference 44 | BuildableIdentifier = "primary" 45 | BlueprintIdentifier = "6393C5DA1C3B229200EB1FD8" 46 | BuildableName = "Haneke.framework" 47 | BlueprintName = "Haneke-tvOS" 48 | ReferencedContainer = "container:Haneke.xcodeproj"> 49 | </BuildableReference> 50 | </MacroExpansion> 51 | <AdditionalOptions> 52 | </AdditionalOptions> 53 | </TestAction> 54 | <LaunchAction 55 | buildConfiguration = "Debug" 56 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 57 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 58 | launchStyle = "0" 59 | useCustomWorkingDirectory = "NO" 60 | ignoresPersistentStateOnLaunch = "NO" 61 | debugDocumentVersioning = "YES" 62 | debugServiceExtension = "internal" 63 | allowLocationSimulation = "YES"> 64 | <MacroExpansion> 65 | <BuildableReference 66 | BuildableIdentifier = "primary" 67 | BlueprintIdentifier = "6393C5DA1C3B229200EB1FD8" 68 | BuildableName = "Haneke.framework" 69 | BlueprintName = "Haneke-tvOS" 70 | ReferencedContainer = "container:Haneke.xcodeproj"> 71 | </BuildableReference> 72 | </MacroExpansion> 73 | <AdditionalOptions> 74 | </AdditionalOptions> 75 | </LaunchAction> 76 | <ProfileAction 77 | buildConfiguration = "Release" 78 | shouldUseLaunchSchemeArgsEnv = "YES" 79 | savedToolIdentifier = "" 80 | useCustomWorkingDirectory = "NO" 81 | debugDocumentVersioning = "YES"> 82 | <MacroExpansion> 83 | <BuildableReference 84 | BuildableIdentifier = "primary" 85 | BlueprintIdentifier = "6393C5DA1C3B229200EB1FD8" 86 | BuildableName = "Haneke.framework" 87 | BlueprintName = "Haneke-tvOS" 88 | ReferencedContainer = "container:Haneke.xcodeproj"> 89 | </BuildableReference> 90 | </MacroExpansion> 91 | </ProfileAction> 92 | <AnalyzeAction 93 | buildConfiguration = "Debug"> 94 | </AnalyzeAction> 95 | <ArchiveAction 96 | buildConfiguration = "Release" 97 | revealArchiveInOrganizer = "YES"> 98 | </ArchiveAction> 99 | </Scheme> 100 | -------------------------------------------------------------------------------- /Haneke.xcodeproj/xcshareddata/xcschemes/HanekeDemo.xcscheme: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Scheme 3 | LastUpgradeVersion = "1020" 4 | version = "1.3"> 5 | <BuildAction 6 | parallelizeBuildables = "YES" 7 | buildImplicitDependencies = "YES"> 8 | <BuildActionEntries> 9 | <BuildActionEntry 10 | buildForTesting = "YES" 11 | buildForRunning = "YES" 12 | buildForProfiling = "YES" 13 | buildForArchiving = "YES" 14 | buildForAnalyzing = "YES"> 15 | <BuildableReference 16 | BuildableIdentifier = "primary" 17 | BlueprintIdentifier = "A0026E9919C9BFBC004DE0C6" 18 | BuildableName = "HanekeDemo.app" 19 | BlueprintName = "HanekeDemo" 20 | ReferencedContainer = "container:Haneke.xcodeproj"> 21 | </BuildableReference> 22 | </BuildActionEntry> 23 | <BuildActionEntry 24 | buildForTesting = "YES" 25 | buildForRunning = "YES" 26 | buildForProfiling = "NO" 27 | buildForArchiving = "NO" 28 | buildForAnalyzing = "YES"> 29 | <BuildableReference 30 | BuildableIdentifier = "primary" 31 | BlueprintIdentifier = "A0026EAD19C9BFBC004DE0C6" 32 | BuildableName = "HanekeDemoTests.xctest" 33 | BlueprintName = "HanekeDemoTests" 34 | ReferencedContainer = "container:Haneke.xcodeproj"> 35 | </BuildableReference> 36 | </BuildActionEntry> 37 | </BuildActionEntries> 38 | </BuildAction> 39 | <TestAction 40 | buildConfiguration = "Debug" 41 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 42 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 43 | shouldUseLaunchSchemeArgsEnv = "YES"> 44 | <Testables> 45 | <TestableReference 46 | skipped = "NO"> 47 | <BuildableReference 48 | BuildableIdentifier = "primary" 49 | BlueprintIdentifier = "A0026EAD19C9BFBC004DE0C6" 50 | BuildableName = "HanekeDemoTests.xctest" 51 | BlueprintName = "HanekeDemoTests" 52 | ReferencedContainer = "container:Haneke.xcodeproj"> 53 | </BuildableReference> 54 | </TestableReference> 55 | </Testables> 56 | <MacroExpansion> 57 | <BuildableReference 58 | BuildableIdentifier = "primary" 59 | BlueprintIdentifier = "A0026E9919C9BFBC004DE0C6" 60 | BuildableName = "HanekeDemo.app" 61 | BlueprintName = "HanekeDemo" 62 | ReferencedContainer = "container:Haneke.xcodeproj"> 63 | </BuildableReference> 64 | </MacroExpansion> 65 | <AdditionalOptions> 66 | </AdditionalOptions> 67 | </TestAction> 68 | <LaunchAction 69 | buildConfiguration = "Debug" 70 | selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" 71 | selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" 72 | launchStyle = "0" 73 | useCustomWorkingDirectory = "NO" 74 | ignoresPersistentStateOnLaunch = "NO" 75 | debugDocumentVersioning = "YES" 76 | debugServiceExtension = "internal" 77 | allowLocationSimulation = "YES"> 78 | <BuildableProductRunnable 79 | runnableDebuggingMode = "0"> 80 | <BuildableReference 81 | BuildableIdentifier = "primary" 82 | BlueprintIdentifier = "A0026E9919C9BFBC004DE0C6" 83 | BuildableName = "HanekeDemo.app" 84 | BlueprintName = "HanekeDemo" 85 | ReferencedContainer = "container:Haneke.xcodeproj"> 86 | </BuildableReference> 87 | </BuildableProductRunnable> 88 | <AdditionalOptions> 89 | </AdditionalOptions> 90 | </LaunchAction> 91 | <ProfileAction 92 | buildConfiguration = "Release" 93 | shouldUseLaunchSchemeArgsEnv = "YES" 94 | savedToolIdentifier = "" 95 | useCustomWorkingDirectory = "NO" 96 | debugDocumentVersioning = "YES"> 97 | <BuildableProductRunnable 98 | runnableDebuggingMode = "0"> 99 | <BuildableReference 100 | BuildableIdentifier = "primary" 101 | BlueprintIdentifier = "A0026E9919C9BFBC004DE0C6" 102 | BuildableName = "HanekeDemo.app" 103 | BlueprintName = "HanekeDemo" 104 | ReferencedContainer = "container:Haneke.xcodeproj"> 105 | </BuildableReference> 106 | </BuildableProductRunnable> 107 | </ProfileAction> 108 | <AnalyzeAction 109 | buildConfiguration = "Debug"> 110 | </AnalyzeAction> 111 | <ArchiveAction 112 | buildConfiguration = "Release" 113 | revealArchiveInOrganizer = "YES"> 114 | </ArchiveAction> 115 | </Scheme> 116 | -------------------------------------------------------------------------------- /Haneke.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <Workspace 3 | version = "1.0"> 4 | <FileRef 5 | location = "container:Haneke.xcodeproj"> 6 | </FileRef> 7 | </Workspace> 8 | -------------------------------------------------------------------------------- /Haneke.xcworkspace/xcshareddata/Haneke.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "C93564C1AFB8B50FF58188F21182F5D0EEAD1C3F", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "38C2A0D4F62B675E8C16C8BC1437C7753846C8AC" : 0, 8 | "C93564C1AFB8B50FF58188F21182F5D0EEAD1C3F" : 0 9 | }, 10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "D9D86AAE-952B-48A9-A2AB-D2832C98D4A1", 11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 12 | "38C2A0D4F62B675E8C16C8BC1437C7753846C8AC" : "HanekeSwiftHanekeTests\/Submodules\/OHHTTPStubs", 13 | "C93564C1AFB8B50FF58188F21182F5D0EEAD1C3F" : "HanekeSwift\/" 14 | }, 15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Haneke", 16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Haneke.xcworkspace", 18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 19 | { 20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/AliSoftware\/OHHTTPStubs.git", 21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "38C2A0D4F62B675E8C16C8BC1437C7753846C8AC" 23 | }, 24 | { 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/Haneke\/HanekeSwift.git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "C93564C1AFB8B50FF58188F21182F5D0EEAD1C3F" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /Haneke.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>IDEDidComputeMac32BitWarning</key> 6 | <true/> 7 | </dict> 8 | </plist> 9 | -------------------------------------------------------------------------------- /Haneke/CGSize+Swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+Swift.swift 3 | // Haneke 4 | // 5 | // Created by Oriol Blanc Gimeno on 09/09/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGSize { 12 | 13 | func hnk_aspectFillSize(_ size: CGSize) -> CGSize { 14 | let scaleWidth = size.width / self.width 15 | let scaleHeight = size.height / self.height 16 | let scale = max(scaleWidth, scaleHeight) 17 | 18 | let resultSize = CGSize(width: self.width * scale, height: self.height * scale) 19 | return CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height)) 20 | } 21 | 22 | func hnk_aspectFitSize(_ size: CGSize) -> CGSize { 23 | let targetAspect = size.width / size.height 24 | let sourceAspect = self.width / self.height 25 | var resultSize = size 26 | 27 | if (targetAspect > sourceAspect) { 28 | resultSize.width = size.height * sourceAspect 29 | } 30 | else { 31 | resultSize.height = size.width / sourceAspect 32 | } 33 | return CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Haneke/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // Haneke 4 | // 5 | // Created by Luis Ascorbe on 23/07/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // Used to add T to NSCache 12 | class ObjectWrapper : NSObject { 13 | let hnk_value: Any 14 | 15 | init(value: Any) { 16 | self.hnk_value = value 17 | } 18 | } 19 | 20 | extension HanekeGlobals { 21 | 22 | // It'd be better to define this in the Cache class but Swift doesn't allow statics in a generic type 23 | public struct Cache { 24 | 25 | public static let OriginalFormatName = "original" 26 | 27 | public enum ErrorCode : Int { 28 | case objectNotFound = -100 29 | case formatNotFound = -101 30 | } 31 | 32 | } 33 | 34 | } 35 | 36 | open class Cache<T: DataConvertible> where T.Result == T, T : DataRepresentable { 37 | 38 | let name: String 39 | 40 | var memoryWarningObserver : NSObjectProtocol! 41 | 42 | public init(name: String) { 43 | self.name = name 44 | 45 | let notifications = NotificationCenter.default 46 | // Using block-based observer to avoid subclassing NSObject 47 | memoryWarningObserver = notifications.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, 48 | object: nil, 49 | queue: OperationQueue.main, 50 | using: { [unowned self] (notification : Notification!) -> Void in 51 | self.onMemoryWarning() 52 | } 53 | ) 54 | 55 | let originalFormat = Format<T>(name: HanekeGlobals.Cache.OriginalFormatName) 56 | self.addFormat(originalFormat) 57 | } 58 | 59 | deinit { 60 | let notifications = NotificationCenter.default 61 | notifications.removeObserver(memoryWarningObserver as Any, name: UIApplication.didReceiveMemoryWarningNotification, object: nil) 62 | } 63 | 64 | open func set(value: T, key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed: ((T) -> ())? = nil) { 65 | if let (format, memoryCache, diskCache) = self.formats[formatName] { 66 | self.format(value: value, format: format) { formattedValue in 67 | let wrapper = ObjectWrapper(value: formattedValue) 68 | memoryCache.setObject(wrapper, forKey: key as AnyObject) 69 | // Value data is sent as @autoclosure to be executed in the disk cache queue. 70 | diskCache.setData(self.dataFromValue(formattedValue, format: format), key: key) 71 | succeed?(formattedValue) 72 | } 73 | } else { 74 | assertionFailure("Can't set value before adding format") 75 | } 76 | } 77 | 78 | @discardableResult open func fetch(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 79 | let fetch = Cache.buildFetch(failure: fail, success: succeed) 80 | if let (format, memoryCache, diskCache) = self.formats[formatName] { 81 | if let wrapper = memoryCache.object(forKey: key as AnyObject) as? ObjectWrapper, let result = wrapper.hnk_value as? T { 82 | fetch.succeed(result) 83 | diskCache.updateAccessDate(self.dataFromValue(result, format: format), key: key) 84 | return fetch 85 | } 86 | 87 | self.fetchFromDiskCache(diskCache, key: key, memoryCache: memoryCache, failure: { error in 88 | fetch.fail(error) 89 | }) { value in 90 | fetch.succeed(value) 91 | } 92 | 93 | } else { 94 | let localizedFormat = NSLocalizedString("Format %@ not found", comment: "Error description") 95 | let description = String(format:localizedFormat, formatName) 96 | let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue, description: description) 97 | fetch.fail(error) 98 | } 99 | return fetch 100 | } 101 | 102 | @discardableResult open func fetch(fetcher : Fetcher<T>, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 103 | let key = fetcher.key 104 | let fetch = Cache.buildFetch(failure: fail, success: succeed) 105 | self.fetch(key: key, formatName: formatName, failure: { error in 106 | if (error as NSError?)?.code == HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue { 107 | fetch.fail(error) 108 | } 109 | 110 | if let (format, _, _) = self.formats[formatName] { 111 | self.fetchAndSet(fetcher, format: format, failure: { error in 112 | fetch.fail(error) 113 | }) {value in 114 | fetch.succeed(value) 115 | } 116 | } 117 | 118 | // Unreachable code. Formats can't be removed from Cache. 119 | }) { value in 120 | fetch.succeed(value) 121 | } 122 | return fetch 123 | } 124 | 125 | open func remove(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName) { 126 | if let (_, memoryCache, diskCache) = self.formats[formatName] { 127 | memoryCache.removeObject(forKey: key as AnyObject) 128 | diskCache.removeData(with: key) 129 | } 130 | } 131 | 132 | open func removeAll(_ completion: (() -> ())? = nil) { 133 | let group = DispatchGroup() 134 | for (_, (_, memoryCache, diskCache)) in self.formats { 135 | memoryCache.removeAllObjects() 136 | group.enter() 137 | diskCache.removeAllData { 138 | group.leave() 139 | } 140 | } 141 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { 142 | let timeout = DispatchTime.now() + Double(Int64(60 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC) 143 | if group.wait(timeout: timeout) != .success { 144 | Log.error(message: "removeAll timed out waiting for disk caches") 145 | } 146 | let path = self.cachePath 147 | do { 148 | try FileManager.default.removeItem(atPath: path) 149 | } catch { 150 | Log.error(message: "Failed to remove path \(path)", error: error) 151 | } 152 | if let completion = completion { 153 | DispatchQueue.main.async { 154 | completion() 155 | } 156 | } 157 | } 158 | } 159 | 160 | // MARK: Size 161 | 162 | open var size: UInt64 { 163 | var size: UInt64 = 0 164 | for (_, (_, _, diskCache)) in self.formats { 165 | diskCache.cacheQueue.sync { size += diskCache.size } 166 | } 167 | return size 168 | } 169 | 170 | // MARK: Notifications 171 | 172 | func onMemoryWarning() { 173 | for (_, (_, memoryCache, _)) in self.formats { 174 | memoryCache.removeAllObjects() 175 | } 176 | } 177 | 178 | // MARK: Formats 179 | 180 | public var formats : [String : (Format<T>, NSCache<AnyObject, AnyObject>, DiskCache)] = [:] 181 | 182 | open func addFormat(_ format : Format<T>) { 183 | let name = format.name 184 | let formatPath = self.formatPath(withFormatName: name) 185 | let memoryCache = NSCache<AnyObject, AnyObject>() 186 | let diskCache = DiskCache(path: formatPath, capacity : format.diskCapacity) 187 | self.formats[name] = (format, memoryCache, diskCache) 188 | } 189 | 190 | // MARK: Internal 191 | 192 | lazy var cachePath: String = { 193 | let basePath = DiskCache.basePath() 194 | let cachePath = (basePath as NSString).appendingPathComponent(self.name) 195 | return cachePath 196 | }() 197 | 198 | func formatPath(withFormatName formatName: String) -> String { 199 | let formatPath = (self.cachePath as NSString).appendingPathComponent(formatName) 200 | do { 201 | try FileManager.default.createDirectory(atPath: formatPath, withIntermediateDirectories: true, attributes: nil) 202 | } catch { 203 | Log.error(message: "Failed to create directory \(formatPath)", error: error) 204 | } 205 | return formatPath 206 | } 207 | 208 | // MARK: Private 209 | 210 | func dataFromValue(_ value : T, format : Format<T>) -> Data? { 211 | if let data = format.convertToData?(value) { 212 | return data as Data 213 | } 214 | return value.asData() 215 | } 216 | 217 | fileprivate func fetchFromDiskCache(_ diskCache : DiskCache, key: String, memoryCache : NSCache<AnyObject, AnyObject>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) { 218 | diskCache.fetchData(key: key, failure: { error in 219 | if let block = fail { 220 | if (error as NSError?)?.code == NSFileReadNoSuchFileError { 221 | let localizedFormat = NSLocalizedString("Object not found for key %@", comment: "Error description") 222 | let description = String(format:localizedFormat, key) 223 | let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.objectNotFound.rawValue, description: description) 224 | block(error) 225 | } else { 226 | block(error) 227 | } 228 | } 229 | }) { data in 230 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: { 231 | let value = T.convertFromData(data) 232 | if let value = value { 233 | let descompressedValue = self.decompressedImageIfNeeded(value) 234 | DispatchQueue.main.async(execute: { 235 | succeed(descompressedValue) 236 | let wrapper = ObjectWrapper(value: descompressedValue) 237 | memoryCache.setObject(wrapper, forKey: key as AnyObject) 238 | }) 239 | } 240 | }) 241 | } 242 | } 243 | 244 | fileprivate func fetchAndSet(_ fetcher : Fetcher<T>, format : Format<T>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) { 245 | fetcher.fetch(failure: { error in 246 | let _ = fail?(error) 247 | }) { value in 248 | self.set(value: value, key: fetcher.key, formatName: format.name, success: succeed) 249 | } 250 | } 251 | 252 | fileprivate func format(value : T, format : Format<T>, success succeed : @escaping (T) -> ()) { 253 | // HACK: Ideally Cache shouldn't treat images differently but I can't think of any other way of doing this that doesn't complicate the API for other types. 254 | if format.isIdentity && !(value is UIImage) { 255 | succeed(value) 256 | } else { 257 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { 258 | var formatted = format.apply(value) 259 | 260 | if let formattedImage = formatted as? UIImage { 261 | let originalImage = value as? UIImage 262 | if formattedImage === originalImage { 263 | formatted = self.decompressedImageIfNeeded(formatted) 264 | } 265 | } 266 | 267 | DispatchQueue.main.async { 268 | succeed(formatted) 269 | } 270 | } 271 | } 272 | } 273 | 274 | fileprivate func decompressedImageIfNeeded(_ value : T) -> T { 275 | if let image = value as? UIImage { 276 | let decompressedImage = image.hnk_decompressedImage() as? T 277 | return decompressedImage! 278 | } 279 | return value 280 | } 281 | 282 | fileprivate class func buildFetch(failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 283 | let fetch = Fetch<T>() 284 | if let succeed = succeed { 285 | fetch.onSuccess(succeed) 286 | } 287 | if let fail = fail { 288 | fetch.onFailure(fail) 289 | } 290 | return fetch 291 | } 292 | 293 | // MARK: Convenience fetch 294 | // Ideally we would put each of these in the respective fetcher file as a Cache extension. Unfortunately, this fails to link when using the framework in a project as of Xcode 6.1. 295 | 296 | open func fetch(key: String, value getValue : @autoclosure @escaping () -> T.Result, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 297 | let fetcher = SimpleFetcher<T>(key: key, value: getValue()) 298 | return self.fetch(fetcher: fetcher, formatName: formatName, success: succeed) 299 | } 300 | 301 | open func fetch(path: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 302 | let fetcher = DiskFetcher<T>(path: path) 303 | return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed) 304 | } 305 | 306 | open func fetch(URL : Foundation.URL, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> { 307 | let fetcher = NetworkFetcher<T>(URL: URL) 308 | return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed) 309 | } 310 | 311 | } 312 | -------------------------------------------------------------------------------- /Haneke/CryptoSwiftMD5.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoSwiftMD5.Swif 3 | // 4 | // To date, adding CommonCrypto to a Swift framework is problematic. See: 5 | // http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework 6 | // We're using a subset of CryptoSwift as a (temporary?) alternative. 7 | // The following is an altered source version that only includes MD5. The original software can be found at: 8 | // https://github.com/krzyzanowskim/CryptoSwift 9 | // This is the original copyright notice: 10 | 11 | /* 12 | Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com> 13 | This software is provided 'as-is', without any express or implied warranty. 14 | 15 | In no event will the authors be held liable for any damages arising from the use of this software. 16 | 17 | Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 18 | 19 | - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. 20 | - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 21 | - This notice may not be removed or altered from any source or binary distribution. 22 | */ 23 | 24 | import Foundation 25 | 26 | /** array of bytes, little-endian representation */ 27 | func arrayOfBytes<T>(value:T, length:Int? = nil) -> [UInt8] { 28 | let totalBytes = length ?? MemoryLayout<T>.size 29 | 30 | let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1) 31 | valuePointer.pointee = value 32 | 33 | let bytesPointer = UnsafeMutablePointer<UInt8>(OpaquePointer(valuePointer)) 34 | var bytes = Array<UInt8>(repeating: 0, count: totalBytes) 35 | for j in 0..<min(MemoryLayout<T>.size,totalBytes) { 36 | bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee 37 | } 38 | 39 | valuePointer.deinitialize(count: 1) 40 | valuePointer.deallocate() 41 | 42 | return bytes 43 | } 44 | 45 | extension Int { 46 | /** Array of bytes with optional padding (little-endian) */ 47 | public func bytes(totalBytes: Int = MemoryLayout<Int>.size) -> [UInt8] { 48 | return arrayOfBytes(value: self, length: totalBytes) 49 | } 50 | 51 | } 52 | 53 | extension NSMutableData { 54 | 55 | /** Convenient way to append bytes */ 56 | internal func appendBytes(arrayOfBytes: [UInt8]) { 57 | self.append(arrayOfBytes, length: arrayOfBytes.count) 58 | } 59 | 60 | } 61 | 62 | struct BytesSequence: Sequence { 63 | let chunkSize: Int 64 | let data: [UInt8] 65 | 66 | func makeIterator() -> AnyIterator<ArraySlice<UInt8>> { 67 | var offset:Int = 0 68 | return AnyIterator { 69 | let end = Swift.min(self.chunkSize, self.data.count - offset) 70 | let result = self.data[offset..<offset + end] 71 | offset += result.count 72 | return !result.isEmpty ? result : nil 73 | } 74 | } 75 | } 76 | 77 | class HashBase { 78 | 79 | static let size:Int = 16 // 128 / 8 80 | let message: [UInt8] 81 | 82 | init (_ message: [UInt8]) { 83 | self.message = message 84 | } 85 | 86 | /** Common part for hash calculation. Prepare header data. */ 87 | func prepare(_ len:Int) -> [UInt8] { 88 | var tmpMessage = message 89 | 90 | // Step 1. Append Padding Bits 91 | tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message 92 | 93 | // append "0" bit until message length in bits ≡ 448 (mod 512) 94 | var msgLength = tmpMessage.count 95 | var counter = 0 96 | 97 | while msgLength % len != (len - 8) { 98 | counter += 1 99 | msgLength += 1 100 | } 101 | 102 | tmpMessage += Array<UInt8>(repeating: 0, count: counter) 103 | return tmpMessage 104 | } 105 | } 106 | 107 | func rotateLeft(v: UInt32, n: UInt32) -> UInt32 { 108 | return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n)) 109 | } 110 | 111 | func sliceToUInt32Array(_ slice: ArraySlice<UInt8>) -> [UInt32] { 112 | var result = [UInt32]() 113 | result.reserveCapacity(16) 114 | for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) { 115 | let val1:UInt32 = (UInt32(slice[idx.advanced(by: 3)]) << 24) 116 | let val2:UInt32 = (UInt32(slice[idx.advanced(by: 2)]) << 16) 117 | let val3:UInt32 = (UInt32(slice[idx.advanced(by: 1)]) << 8) 118 | let val4:UInt32 = UInt32(slice[idx]) 119 | let val:UInt32 = val1 | val2 | val3 | val4 120 | result.append(val) 121 | } 122 | return result 123 | } 124 | 125 | class MD5 : HashBase { 126 | 127 | 128 | /** specifies the per-round shift amounts */ 129 | private let s: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 130 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 131 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 132 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] 133 | 134 | /** binary integer part of the sines of integers (Radians) */ 135 | private let k: [UInt32] = [0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee, 136 | 0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501, 137 | 0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be, 138 | 0x6b901122,0xfd987193,0xa679438e,0x49b40821, 139 | 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa, 140 | 0xd62f105d,0x2441453,0xd8a1e681,0xe7d3fbc8, 141 | 0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed, 142 | 0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a, 143 | 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c, 144 | 0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70, 145 | 0x289b7ec6,0xeaa127fa,0xd4ef3085,0x4881d05, 146 | 0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665, 147 | 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039, 148 | 0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1, 149 | 0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1, 150 | 0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391] 151 | 152 | private let h: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] 153 | 154 | func calculate() -> [UInt8] { 155 | var tmpMessage = prepare(64) 156 | tmpMessage.reserveCapacity(tmpMessage.count + 4) 157 | 158 | // initialize hh with hash values 159 | var hh = h 160 | 161 | // Step 2. Append Length a 64-bit representation of lengthInBits 162 | let lengthInBits = (message.count * 8) 163 | let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8) 164 | tmpMessage += lengthBytes.reversed() 165 | 166 | // Process the message in successive 512-bit chunks: 167 | let chunkSizeBytes = 512 / 8 // 64 168 | for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { 169 | // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 170 | var M = sliceToUInt32Array(chunk) 171 | assert(M.count == 16, "Invalid array") 172 | 173 | // Initialize hash value for this chunk: 174 | var A:UInt32 = hh[0] 175 | var B:UInt32 = hh[1] 176 | var C:UInt32 = hh[2] 177 | var D:UInt32 = hh[3] 178 | 179 | var dTemp:UInt32 = 0 180 | 181 | // Main loop 182 | for j in 0..<k.count { 183 | var g = 0 184 | var F:UInt32 = 0 185 | 186 | switch (j) { 187 | case 0...15: 188 | F = (B & C) | ((~B) & D) 189 | g = j 190 | break 191 | case 16...31: 192 | F = (D & B) | (~D & C) 193 | g = (5 * j + 1) % 16 194 | break 195 | case 32...47: 196 | F = B ^ C ^ D 197 | g = (3 * j + 5) % 16 198 | break 199 | case 48...63: 200 | F = C ^ (B | (~D)) 201 | g = (7 * j) % 16 202 | break 203 | default: 204 | break 205 | } 206 | dTemp = D 207 | D = C 208 | C = B 209 | B = B &+ rotateLeft(v: A &+ F &+ k[j] &+ M[g], n: s[j]) 210 | A = dTemp 211 | } 212 | 213 | hh[0] = hh[0] &+ A 214 | hh[1] = hh[1] &+ B 215 | hh[2] = hh[2] &+ C 216 | hh[3] = hh[3] &+ D 217 | } 218 | 219 | var result = [UInt8]() 220 | result.reserveCapacity(hh.count / 4) 221 | 222 | hh.forEach { 223 | let itemLE = $0.littleEndian 224 | result += [UInt8(itemLE & 0xff), UInt8((itemLE >> 8) & 0xff), UInt8((itemLE >> 16) & 0xff), UInt8((itemLE >> 24) & 0xff)] 225 | } 226 | 227 | return result 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /Haneke/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/19/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // See: http://stackoverflow.com/questions/25922152/not-identical-to-self 12 | public protocol DataConvertible { 13 | associatedtype Result 14 | 15 | static func convertFromData(_ data:Data) -> Result? 16 | } 17 | 18 | public protocol DataRepresentable { 19 | 20 | func asData() -> Data! 21 | } 22 | 23 | private let imageSync = NSLock() 24 | 25 | extension UIImage : DataConvertible, DataRepresentable { 26 | 27 | public typealias Result = UIImage 28 | 29 | // HACK: UIImage data initializer is no longer thread safe. See: https://github.com/AFNetworking/AFNetworking/issues/2572#issuecomment-115854482 30 | static func safeImageWithData(_ data:Data) -> Result? { 31 | imageSync.lock() 32 | let image = UIImage(data:data, scale: scale) 33 | imageSync.unlock() 34 | return image 35 | } 36 | 37 | public class func convertFromData(_ data: Data) -> Result? { 38 | let image = UIImage.safeImageWithData(data) 39 | return image 40 | } 41 | 42 | public func asData() -> Data! { 43 | return self.hnk_data() 44 | } 45 | 46 | fileprivate static let scale = UIScreen.main.scale 47 | 48 | } 49 | 50 | extension String : DataConvertible, DataRepresentable { 51 | 52 | public typealias Result = String 53 | 54 | public static func convertFromData(_ data: Data) -> Result? { 55 | let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue) 56 | return string as Result? 57 | } 58 | 59 | public func asData() -> Data! { 60 | return self.data(using: String.Encoding.utf8) 61 | } 62 | 63 | } 64 | 65 | extension Data : DataConvertible, DataRepresentable { 66 | 67 | public typealias Result = Data 68 | 69 | public static func convertFromData(_ data: Data) -> Result? { 70 | return data 71 | } 72 | 73 | public func asData() -> Data! { 74 | return self 75 | } 76 | 77 | } 78 | 79 | public enum JSON : DataConvertible, DataRepresentable { 80 | public typealias Result = JSON 81 | 82 | case Dictionary([String:AnyObject]) 83 | case Array([AnyObject]) 84 | 85 | public static func convertFromData(_ data: Data) -> Result? { 86 | do { 87 | let object : Any = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions()) 88 | switch (object) { 89 | case let dictionary as [String:AnyObject]: 90 | return JSON.Dictionary(dictionary) 91 | case let array as [AnyObject]: 92 | return JSON.Array(array) 93 | default: 94 | return nil 95 | } 96 | } catch { 97 | Log.error(message: "Invalid JSON data", error: error) 98 | return nil 99 | } 100 | } 101 | 102 | public func asData() -> Data! { 103 | switch (self) { 104 | case .Dictionary(let dictionary): 105 | return try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions()) 106 | case .Array(let array): 107 | return try? JSONSerialization.data(withJSONObject: array, options: JSONSerialization.WritingOptions()) 108 | } 109 | } 110 | 111 | public var array : [AnyObject]! { 112 | switch (self) { 113 | case .Dictionary(_): 114 | return nil 115 | case .Array(let array): 116 | return array 117 | } 118 | } 119 | 120 | public var dictionary : [String:AnyObject]! { 121 | switch (self) { 122 | case .Dictionary(let dictionary): 123 | return dictionary 124 | case .Array(_): 125 | return nil 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /Haneke/DiskCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskCache.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class DiskCache { 12 | 13 | open class func basePath() -> String { 14 | let cachesPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] 15 | let hanekePathComponent = HanekeGlobals.Domain 16 | let basePath = (cachesPath as NSString).appendingPathComponent(hanekePathComponent) 17 | // TODO: Do not recaculate basePath value 18 | return basePath 19 | } 20 | 21 | public let path: String 22 | 23 | open var size : UInt64 = 0 24 | 25 | open var capacity : UInt64 = 0 { 26 | didSet { 27 | self.cacheQueue.async(execute: { 28 | self.controlCapacity() 29 | }) 30 | } 31 | } 32 | 33 | open lazy var cacheQueue : DispatchQueue = { 34 | let queueName = HanekeGlobals.Domain + "." + (self.path as NSString).lastPathComponent 35 | let cacheQueue = DispatchQueue(label: queueName, attributes: []) 36 | return cacheQueue 37 | }() 38 | 39 | public init(path: String, capacity: UInt64 = UINT64_MAX) { 40 | self.path = path 41 | self.capacity = capacity 42 | self.cacheQueue.async(execute: { 43 | self.calculateSize() 44 | self.controlCapacity() 45 | }) 46 | } 47 | 48 | open func setData( _ getData: @autoclosure @escaping () -> Data?, key: String) { 49 | cacheQueue.async(execute: { 50 | if let data = getData() { 51 | self.setDataSync(data, key: key) 52 | } else { 53 | Log.error(message: "Failed to get data for key \(key)") 54 | } 55 | }) 56 | } 57 | 58 | open func fetchData(key: String, failure fail: ((Error?) -> ())? = nil, success succeed: @escaping (Data) -> ()) { 59 | cacheQueue.async { 60 | let path = self.path(forKey: key) 61 | do { 62 | let data = try Data(contentsOf: URL(fileURLWithPath: path), options: Data.ReadingOptions()) 63 | DispatchQueue.main.async { 64 | succeed(data) 65 | } 66 | self.updateDiskAccessDate(atPath: path) 67 | } catch { 68 | if let block = fail { 69 | DispatchQueue.main.async { 70 | block(error) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | open func removeData(with key: String) { 78 | cacheQueue.async(execute: { 79 | let path = self.path(forKey: key) 80 | self.removeFile(atPath: path) 81 | }) 82 | } 83 | 84 | open func removeAllData(_ completion: (() -> ())? = nil) { 85 | let fileManager = FileManager.default 86 | let cachePath = self.path 87 | cacheQueue.async(execute: { 88 | do { 89 | let contents = try fileManager.contentsOfDirectory(atPath: cachePath) 90 | for pathComponent in contents { 91 | let path = (cachePath as NSString).appendingPathComponent(pathComponent) 92 | do { 93 | try fileManager.removeItem(atPath: path) 94 | } catch { 95 | Log.error(message: "Failed to remove path \(path)", error: error) 96 | } 97 | } 98 | self.calculateSize() 99 | } catch { 100 | Log.error(message: "Failed to list directory", error: error) 101 | } 102 | if let completion = completion { 103 | DispatchQueue.main.async { 104 | completion() 105 | } 106 | } 107 | }) 108 | } 109 | 110 | open func updateAccessDate( _ getData: @autoclosure @escaping () -> Data?, key: String) { 111 | cacheQueue.async(execute: { 112 | let path = self.path(forKey: key) 113 | let fileManager = FileManager.default 114 | if (!(fileManager.fileExists(atPath: path) && self.updateDiskAccessDate(atPath: path))){ 115 | if let data = getData() { 116 | self.setDataSync(data, key: key) 117 | } else { 118 | Log.error(message: "Failed to get data for key \(key)") 119 | } 120 | } 121 | }) 122 | } 123 | 124 | open func path(forKey key: String) -> String { 125 | let escapedFilename = key.escapedFilename() 126 | let filename = escapedFilename.count < Int(NAME_MAX) ? escapedFilename : key.MD5Filename() 127 | let keyPath = (self.path as NSString).appendingPathComponent(filename) 128 | return keyPath 129 | } 130 | 131 | // MARK: Private 132 | 133 | fileprivate func calculateSize() { 134 | let fileManager = FileManager.default 135 | size = 0 136 | let cachePath = self.path 137 | do { 138 | let contents = try fileManager.contentsOfDirectory(atPath: cachePath) 139 | for pathComponent in contents { 140 | let path = (cachePath as NSString).appendingPathComponent(pathComponent) 141 | do { 142 | let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path) 143 | if let fileSize = attributes[FileAttributeKey.size] as? UInt64 { 144 | size += fileSize 145 | } 146 | } catch { 147 | Log.error(message: "Failed to list directory", error: error) 148 | } 149 | } 150 | 151 | } catch { 152 | Log.error(message: "Failed to list directory", error: error) 153 | } 154 | } 155 | 156 | fileprivate func controlCapacity() { 157 | if self.size <= self.capacity { return } 158 | 159 | let fileManager = FileManager.default 160 | let cachePath = self.path 161 | fileManager.enumerateContentsOfDirectory(atPath: cachePath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: true) { (URL : URL, _, stop : inout Bool) -> Void in 162 | 163 | self.removeFile(atPath: URL.path) 164 | 165 | stop = self.size <= self.capacity 166 | } 167 | } 168 | 169 | fileprivate func setDataSync(_ data: Data, key: String) { 170 | let path = self.path(forKey: key) 171 | let fileManager = FileManager.default 172 | let previousAttributes : [FileAttributeKey: Any]? = try? fileManager.attributesOfItem(atPath: path) 173 | 174 | do { 175 | try data.write(to: URL(fileURLWithPath: path), options: Data.WritingOptions.atomicWrite) 176 | } catch { 177 | Log.error(message: "Failed to write key \(key)", error: error) 178 | } 179 | 180 | if let attributes = previousAttributes { 181 | if let fileSize = attributes[FileAttributeKey.size] as? UInt64 { 182 | substract(size: fileSize) 183 | } 184 | } 185 | self.size += UInt64(data.count) 186 | self.controlCapacity() 187 | } 188 | 189 | @discardableResult fileprivate func updateDiskAccessDate(atPath path: String) -> Bool { 190 | let fileManager = FileManager.default 191 | let now = Date() 192 | do { 193 | try fileManager.setAttributes([FileAttributeKey.modificationDate : now], ofItemAtPath: path) 194 | return true 195 | } catch { 196 | Log.error(message: "Failed to update access date", error: error) 197 | return false 198 | } 199 | } 200 | 201 | fileprivate func removeFile(atPath path: String) { 202 | let fileManager = FileManager.default 203 | do { 204 | let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path) 205 | do { 206 | try fileManager.removeItem(atPath: path) 207 | if let fileSize = attributes[FileAttributeKey.size] as? UInt64 { 208 | substract(size: fileSize) 209 | } 210 | } catch { 211 | Log.error(message: "Failed to remove file", error: error) 212 | } 213 | } catch { 214 | if isNoSuchFileError(error) { 215 | Log.debug(message: "File not found", error: error) 216 | } else { 217 | Log.error(message: "Failed to remove file", error: error) 218 | } 219 | } 220 | } 221 | 222 | fileprivate func substract(size : UInt64) { 223 | if (self.size >= size) { 224 | self.size -= size 225 | } else { 226 | Log.error(message: "Disk cache size (\(self.size)) is smaller than size to substract (\(size))") 227 | self.size = 0 228 | } 229 | } 230 | } 231 | 232 | private func isNoSuchFileError(_ error : Error?) -> Bool { 233 | if let error = error { 234 | return NSCocoaErrorDomain == (error as NSError).domain && (error as NSError).code == NSFileReadNoSuchFileError 235 | } 236 | return false 237 | } 238 | -------------------------------------------------------------------------------- /Haneke/DiskFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskFetcher.swift 3 | // Haneke 4 | // 5 | // Created by Joan Romano on 9/16/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension HanekeGlobals { 12 | 13 | // It'd be better to define this in the DiskFetcher class but Swift doesn't allow to declare an enum in a generic type 14 | public struct DiskFetcher { 15 | 16 | public enum ErrorCode : Int { 17 | case invalidData = -500 18 | } 19 | 20 | } 21 | 22 | } 23 | 24 | open class DiskFetcher<T : DataConvertible> : Fetcher<T> { 25 | 26 | let path: String 27 | var cancelled = false 28 | 29 | public init(path: String) { 30 | self.path = path 31 | let key = path 32 | super.init(key: key) 33 | } 34 | 35 | // MARK: Fetcher 36 | 37 | 38 | open override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 39 | self.cancelled = false 40 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: { [weak self] in 41 | if let strongSelf = self { 42 | strongSelf.privateFetch(failure: fail, success: succeed) 43 | } 44 | }) 45 | } 46 | 47 | open override func cancelFetch() { 48 | self.cancelled = true 49 | } 50 | 51 | // MARK: Private 52 | 53 | fileprivate func privateFetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 54 | if self.cancelled { 55 | return 56 | } 57 | 58 | let data : Data 59 | do { 60 | data = try Data(contentsOf: URL(fileURLWithPath: self.path), options: Data.ReadingOptions()) 61 | } catch { 62 | DispatchQueue.main.async { 63 | if self.cancelled { 64 | return 65 | } 66 | fail(error) 67 | } 68 | return 69 | } 70 | 71 | if self.cancelled { 72 | return 73 | } 74 | 75 | guard let value : T.Result = T.convertFromData(data) else { 76 | let localizedFormat = NSLocalizedString("Failed to convert value from data at path %@", comment: "Error description") 77 | let description = String(format:localizedFormat, self.path) 78 | let error = errorWithCode(HanekeGlobals.DiskFetcher.ErrorCode.invalidData.rawValue, description: description) 79 | DispatchQueue.main.async { 80 | fail(error) 81 | } 82 | return 83 | } 84 | 85 | DispatchQueue.main.async(execute: { 86 | if self.cancelled { 87 | return 88 | } 89 | succeed(value) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Haneke/Fetch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fetch.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/28/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum FetchState<T> { 12 | case pending 13 | // Using Wrapper as a workaround for error 'unimplemented IR generation feature non-fixed multi-payload enum layout' 14 | // See: http://swiftradar.tumblr.com/post/88314603360/swift-fails-to-compile-enum-with-two-data-cases 15 | // See: http://owensd.io/2014/08/06/fixed-enum-layout.html 16 | case success(Wrapper<T>) 17 | case failure(Error?) 18 | } 19 | 20 | open class Fetch<T> { 21 | 22 | public typealias Succeeder = (T) -> () 23 | 24 | public typealias Failer = (Error?) -> () 25 | 26 | fileprivate var onSuccess : Succeeder? 27 | 28 | fileprivate var onFailure : Failer? 29 | 30 | fileprivate var state : FetchState<T> = FetchState.pending 31 | 32 | public init() {} 33 | 34 | @discardableResult open func onSuccess(_ onSuccess: @escaping Succeeder) -> Self { 35 | self.onSuccess = onSuccess 36 | switch self.state { 37 | case FetchState.success(let wrapper): 38 | onSuccess(wrapper.value) 39 | default: 40 | break 41 | } 42 | return self 43 | } 44 | 45 | @discardableResult open func onFailure(_ onFailure: @escaping Failer) -> Self { 46 | self.onFailure = onFailure 47 | switch self.state { 48 | case FetchState.failure(let error): 49 | onFailure(error) 50 | default: 51 | break 52 | } 53 | return self 54 | } 55 | 56 | func succeed(_ value: T) { 57 | self.state = FetchState.success(Wrapper(value)) 58 | self.onSuccess?(value) 59 | } 60 | 61 | func fail(_ error: Error? = nil) { 62 | self.state = FetchState.failure(error) 63 | self.onFailure?(error) 64 | } 65 | 66 | var hasFailed : Bool { 67 | switch self.state { 68 | case FetchState.failure(_): 69 | return true 70 | default: 71 | return false 72 | } 73 | } 74 | 75 | var hasSucceeded : Bool { 76 | switch self.state { 77 | case FetchState.success(_): 78 | return true 79 | default: 80 | return false 81 | } 82 | } 83 | 84 | } 85 | 86 | open class Wrapper<T> { 87 | public let value: T 88 | public init(_ value: T) { self.value = value } 89 | } 90 | -------------------------------------------------------------------------------- /Haneke/Fetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fetcher.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/9/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // See: http://stackoverflow.com/questions/25915306/generic-closure-in-protocol 12 | open class Fetcher<T : DataConvertible> { 13 | 14 | public let key: String 15 | 16 | public init(key: String) { 17 | self.key = key 18 | } 19 | 20 | open func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {} 21 | 22 | open func cancelFetch() {} 23 | } 24 | 25 | class SimpleFetcher<T : DataConvertible> : Fetcher<T> { 26 | 27 | let getValue : () -> T.Result 28 | 29 | init(key: String, value getValue : @autoclosure @escaping () -> T.Result) { 30 | self.getValue = getValue 31 | super.init(key: key) 32 | } 33 | 34 | override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 35 | let value = getValue() 36 | succeed(value) 37 | } 38 | 39 | override func cancelFetch() {} 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Haneke/Format.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Format.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/27/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct Format<T> { 12 | 13 | public let name: String 14 | 15 | public let diskCapacity : UInt64 16 | 17 | public var transform : ((T) -> (T))? 18 | 19 | public var convertToData : ((T) -> Data)? 20 | 21 | public init(name: String, diskCapacity : UInt64 = UINT64_MAX, transform: ((T) -> (T))? = nil) { 22 | self.name = name 23 | self.diskCapacity = diskCapacity 24 | self.transform = transform 25 | } 26 | 27 | public func apply(_ value : T) -> T { 28 | var transformed = value 29 | if let transform = self.transform { 30 | transformed = transform(value) 31 | } 32 | return transformed 33 | } 34 | 35 | var isIdentity : Bool { 36 | return self.transform == nil 37 | } 38 | 39 | } 40 | 41 | public struct ImageResizer { 42 | 43 | public enum ScaleMode: String { 44 | case Fill = "fill", AspectFit = "aspectfit", AspectFill = "aspectfill", None = "none" 45 | } 46 | 47 | public typealias T = UIImage 48 | 49 | public let allowUpscaling : Bool 50 | 51 | public let size : CGSize 52 | 53 | public let scaleMode: ScaleMode 54 | 55 | public let compressionQuality : Float 56 | 57 | public init(size: CGSize = CGSize.zero, scaleMode: ScaleMode = .None, allowUpscaling: Bool = true, compressionQuality: Float = 1.0) { 58 | self.size = size 59 | self.scaleMode = scaleMode 60 | self.allowUpscaling = allowUpscaling 61 | self.compressionQuality = compressionQuality 62 | } 63 | 64 | public func resizeImage(_ image: UIImage) -> UIImage { 65 | var resizeToSize: CGSize 66 | switch self.scaleMode { 67 | case .Fill: 68 | resizeToSize = self.size 69 | case .AspectFit: 70 | resizeToSize = image.size.hnk_aspectFitSize(self.size) 71 | case .AspectFill: 72 | resizeToSize = image.size.hnk_aspectFillSize(self.size) 73 | case .None: 74 | return image 75 | } 76 | assert(self.size.width > 0 && self.size.height > 0, "Expected non-zero size. Use ScaleMode.None to avoid resizing.") 77 | 78 | // If does not allow to scale up the image 79 | if (!self.allowUpscaling) { 80 | if (resizeToSize.width > image.size.width || resizeToSize.height > image.size.height) { 81 | return image 82 | } 83 | } 84 | 85 | // Avoid unnecessary computations 86 | if (resizeToSize.width == image.size.width && resizeToSize.height == image.size.height) { 87 | return image 88 | } 89 | 90 | let resizedImage = image.hnk_imageByScaling(toSize: resizeToSize) 91 | return resizedImage 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Haneke/Haneke.h: -------------------------------------------------------------------------------- 1 | // 2 | // Haneke.h 3 | // Haneke 4 | // 5 | // Created by Luis Ascorbe on 23/07/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | #import <UIKit/UIKit.h> 10 | 11 | //! Project version number for Haneke. 12 | FOUNDATION_EXPORT double HanekeVersionNumber; 13 | 14 | //! Project version string for Haneke. 15 | FOUNDATION_EXPORT const unsigned char HanekeVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import <Haneke/PublicHeader.h> 18 | 19 | 20 | -------------------------------------------------------------------------------- /Haneke/Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/9/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct HanekeGlobals { 12 | 13 | public static let Domain = "io.haneke" 14 | 15 | } 16 | 17 | public struct Shared { 18 | 19 | public static var imageCache : Cache<UIImage> { 20 | struct Static { 21 | static let name = "shared-images" 22 | static let cache = Cache<UIImage>(name: name) 23 | } 24 | return Static.cache 25 | } 26 | 27 | public static var dataCache : Cache<Data> { 28 | struct Static { 29 | static let name = "shared-data" 30 | static let cache = Cache<Data>(name: name) 31 | } 32 | return Static.cache 33 | } 34 | 35 | public static var stringCache : Cache<String> { 36 | struct Static { 37 | static let name = "shared-strings" 38 | static let cache = Cache<String>(name: name) 39 | } 40 | return Static.cache 41 | } 42 | 43 | public static var JSONCache : Cache<JSON> { 44 | struct Static { 45 | static let name = "shared-json" 46 | static let cache = Cache<JSON>(name: name) 47 | } 48 | return Static.cache 49 | } 50 | } 51 | 52 | func errorWithCode(_ code: Int, description: String) -> Error { 53 | let userInfo = [NSLocalizedDescriptionKey: description] 54 | return NSError(domain: HanekeGlobals.Domain, code: code, userInfo: userInfo) as Error 55 | } 56 | -------------------------------------------------------------------------------- /Haneke/Info-iOS.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>${EXECUTABLE_NAME}</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>${PRODUCT_NAME}</string> 15 | <key>CFBundlePackageType</key> 16 | <string>FMWK</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.1</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>${CURRENT_PROJECT_VERSION}</string> 23 | <key>NSPrincipalClass</key> 24 | <string></string> 25 | </dict> 26 | </plist> 27 | -------------------------------------------------------------------------------- /Haneke/Info-tvOS.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>${EXECUTABLE_NAME}</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>${PRODUCT_NAME}</string> 15 | <key>CFBundlePackageType</key> 16 | <string>FMWK</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.1</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>${CURRENT_PROJECT_VERSION}</string> 23 | <key>NSPrincipalClass</key> 24 | <string></string> 25 | </dict> 26 | </plist> 27 | -------------------------------------------------------------------------------- /Haneke/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 11/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Log { 12 | 13 | fileprivate static let Tag = "[HANEKE]" 14 | 15 | fileprivate enum Level : String { 16 | case Debug = "[DEBUG]" 17 | case Error = "[ERROR]" 18 | } 19 | 20 | fileprivate static func log(_ level: Level, _ message: @autoclosure () -> String, _ error: Error? = nil) { 21 | if let error = error { 22 | print("\(Tag)\(level.rawValue) \(message()) with error \(error)") 23 | } else { 24 | print("\(Tag)\(level.rawValue) \(message())") 25 | } 26 | } 27 | 28 | static func debug(message: @autoclosure () -> String, error: Error? = nil) { 29 | #if DEBUG 30 | log(.Debug, message(), error) 31 | #endif 32 | } 33 | 34 | static func error(message: @autoclosure () -> String, error: Error? = nil) { 35 | log(.Error, message(), error) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Haneke/NSFileManager+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/26/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension FileManager { 12 | 13 | func enumerateContentsOfDirectory(atPath path: String, orderedByProperty property: String, ascending: Bool, usingBlock block: (URL, Int, inout Bool) -> Void ) { 14 | 15 | let directoryURL = URL(fileURLWithPath: path) 16 | do { 17 | let contents = try self.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [URLResourceKey(rawValue: property)], options: FileManager.DirectoryEnumerationOptions()) 18 | let sortedContents = contents.sorted(by: {(URL1: URL, URL2: URL) -> Bool in 19 | 20 | // Maybe there's a better way to do this. See: http://stackoverflow.com/questions/25502914/comparing-anyobject-in-swift 21 | 22 | var value1 : AnyObject? 23 | do { 24 | try (URL1 as NSURL).getResourceValue(&value1, forKey: URLResourceKey(rawValue: property)) 25 | } catch { 26 | return true 27 | } 28 | var value2 : AnyObject? 29 | do { 30 | try (URL2 as NSURL).getResourceValue(&value2, forKey: URLResourceKey(rawValue: property)) 31 | } catch { 32 | return false 33 | } 34 | 35 | if let string1 = value1 as? String, let string2 = value2 as? String { 36 | return ascending ? string1 < string2 : string2 < string1 37 | } 38 | 39 | if let date1 = value1 as? Date, let date2 = value2 as? Date { 40 | return ascending ? date1 < date2 : date2 < date1 41 | } 42 | 43 | if let number1 = value1 as? NSNumber, let number2 = value2 as? NSNumber { 44 | return ascending ? number1 < number2 : number2 < number1 45 | } 46 | 47 | return false 48 | }) 49 | 50 | for (i, v) in sortedContents.enumerated() { 51 | var stop : Bool = false 52 | block(v, i, &stop) 53 | if stop { break } 54 | } 55 | 56 | } catch { 57 | Log.error(message: "Failed to list directory", error: error) 58 | } 59 | } 60 | 61 | } 62 | 63 | func < (lhs: NSNumber, rhs: NSNumber) -> Bool { 64 | return lhs.compare(rhs) == ComparisonResult.orderedAscending 65 | } 66 | -------------------------------------------------------------------------------- /Haneke/NSHTTPURLResponse+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponse+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 1/2/16. 6 | // Copyright © 2016 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension HTTPURLResponse { 12 | 13 | func hnk_isValidStatusCode() -> Bool { 14 | switch self.statusCode { 15 | case 200...201: 16 | return true 17 | default: 18 | return false 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Haneke/NSURLResponse+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponse+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/12/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLResponse { 12 | 13 | func hnk_validateLength(ofData data: Data) -> Bool { 14 | let expectedContentLength = self.expectedContentLength 15 | if (expectedContentLength > -1) { 16 | let dataLength = data.count 17 | return Int64(dataLength) >= expectedContentLength 18 | } 19 | return true 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Haneke/NetworkFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFetcher.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/12/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension HanekeGlobals { 12 | 13 | // It'd be better to define this in the NetworkFetcher class but Swift doesn't allow to declare an enum in a generic type 14 | public struct NetworkFetcher { 15 | 16 | public enum ErrorCode : Int { 17 | case invalidData = -400 18 | case missingData = -401 19 | case invalidStatusCode = -402 20 | } 21 | 22 | } 23 | 24 | } 25 | 26 | open class NetworkFetcher<T : DataConvertible> : Fetcher<T> { 27 | 28 | let URL : Foundation.URL 29 | 30 | public init(URL : Foundation.URL) { 31 | self.URL = URL 32 | 33 | let key = URL.absoluteString 34 | super.init(key: key) 35 | } 36 | 37 | open var session : URLSession { return URLSession.shared } 38 | 39 | var task : URLSessionDataTask? = nil 40 | 41 | var cancelled = false 42 | 43 | // MARK: Fetcher 44 | 45 | open override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 46 | self.cancelled = false 47 | self.task = self.session.dataTask(with: self.URL) {[weak self] (data, response, error) -> Void in 48 | if let strongSelf = self { 49 | strongSelf.onReceive(data: data, response: response, error: error, failure: fail, success: succeed) 50 | } 51 | } 52 | self.task?.resume() 53 | } 54 | 55 | open override func cancelFetch() { 56 | self.task?.cancel() 57 | self.cancelled = true 58 | } 59 | 60 | // MARK: Private 61 | 62 | fileprivate func onReceive(data: Data!, response: URLResponse!, error: Error!, failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 63 | 64 | if cancelled { return } 65 | 66 | let URL = self.URL 67 | 68 | if let error = error { 69 | if ((error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled) { return } 70 | 71 | Log.debug(message: "Request \(URL.absoluteString) failed", error: error) 72 | DispatchQueue.main.async(execute: { fail(error) }) 73 | return 74 | } 75 | 76 | if let httpResponse = response as? HTTPURLResponse , !httpResponse.hnk_isValidStatusCode() { 77 | let description = HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode) 78 | self.failWithCode(.invalidStatusCode, localizedDescription: description, failure: fail) 79 | return 80 | } 81 | 82 | if !response.hnk_validateLength(ofData: data) { 83 | let localizedFormat = NSLocalizedString("Request expected %ld bytes and received %ld bytes", comment: "Error description") 84 | let description = String(format:localizedFormat, response.expectedContentLength, data.count) 85 | self.failWithCode(.missingData, localizedDescription: description, failure: fail) 86 | return 87 | } 88 | 89 | guard let value = T.convertFromData(data) else { 90 | let localizedFormat = NSLocalizedString("Failed to convert value from data at URL %@", comment: "Error description") 91 | let description = String(format:localizedFormat, URL.absoluteString) 92 | self.failWithCode(.invalidData, localizedDescription: description, failure: fail) 93 | return 94 | } 95 | 96 | DispatchQueue.main.async { succeed(value) } 97 | 98 | } 99 | 100 | fileprivate func failWithCode(_ code: HanekeGlobals.NetworkFetcher.ErrorCode, localizedDescription: String, failure fail: @escaping ((Error?) -> ())) { 101 | let error = errorWithCode(code.rawValue, description: localizedDescription) 102 | Log.debug(message: localizedDescription, error: error) 103 | DispatchQueue.main.async { fail(error) } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Haneke/String+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/30/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | 13 | func escapedFilename() -> String { 14 | return [ "\0":"%00", ":":"%3A", "/":"%2F" ] 15 | .reduce(self.components(separatedBy: "%").joined(separator: "%25")) { 16 | str, m in str.components(separatedBy: m.0).joined(separator: m.1) 17 | } 18 | } 19 | 20 | func MD5String() -> String { 21 | guard let data = self.data(using: String.Encoding.utf8) else { 22 | return self 23 | } 24 | 25 | let MD5Calculator = MD5(Array(data)) 26 | let MD5Data = MD5Calculator.calculate() 27 | let resultBytes = UnsafeMutablePointer<CUnsignedChar>(mutating: MD5Data) 28 | let resultEnumerator = UnsafeBufferPointer<CUnsignedChar>(start: resultBytes, count: MD5Data.count) 29 | let MD5String = NSMutableString() 30 | for c in resultEnumerator { 31 | MD5String.appendFormat("%02x", c) 32 | } 33 | return MD5String as String 34 | } 35 | 36 | func MD5Filename() -> String { 37 | let MD5String = self.MD5String() 38 | 39 | // NSString.pathExtension alone could return a query string, which can lead to very long filenames. 40 | let pathExtension = URL(string: self)?.pathExtension ?? (self as NSString).pathExtension 41 | 42 | if pathExtension.count > 0 { 43 | return (MD5String as NSString).appendingPathExtension(pathExtension) ?? MD5String 44 | } else { 45 | return MD5String 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Haneke/UIButton+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Joan Romano on 10/1/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIButton { 12 | 13 | var hnk_imageFormat : Format<UIImage> { 14 | let bounds = self.bounds 15 | assert(bounds.size.width > 0 && bounds.size.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UIButton size is zero. Set its frame, call sizeToFit or force layout first. You can also set a custom format with a defined size if you don't want to force layout.") 16 | let contentRect = self.contentRect(forBounds: bounds) 17 | let imageInsets = self.imageEdgeInsets 18 | let scaleMode = self.contentHorizontalAlignment != UIControl.ContentHorizontalAlignment.fill || self.contentVerticalAlignment != UIControl.ContentVerticalAlignment.fill ? ImageResizer.ScaleMode.AspectFit : ImageResizer.ScaleMode.Fill 19 | let imageSize = CGSize(width: contentRect.width - imageInsets.left - imageInsets.right, height: contentRect.height - imageInsets.top - imageInsets.bottom) 20 | 21 | return HanekeGlobals.UIKit.formatWithSize(imageSize, scaleMode: scaleMode, allowUpscaling: scaleMode == ImageResizer.ScaleMode.AspectFit ? false : true) 22 | } 23 | 24 | func hnk_setImageFromURL(_ URL: Foundation.URL, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil) { 25 | let fetcher = NetworkFetcher<UIImage>(URL: URL) 26 | self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed) 27 | } 28 | 29 | func hnk_setImage(_ image: UIImage, key: String, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, success succeed: ((UIImage) -> ())? = nil) { 30 | let fetcher = SimpleFetcher<UIImage>(key: key, value: image) 31 | self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, success: succeed) 32 | } 33 | 34 | func hnk_setImageFromFile(_ path: String, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil) { 35 | let fetcher = DiskFetcher<UIImage>(path: path) 36 | self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed) 37 | } 38 | 39 | func hnk_setImageFromFetcher(_ fetcher: Fetcher<UIImage>, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil){ 40 | self.hnk_cancelSetImage() 41 | self.hnk_imageFetcher = fetcher 42 | 43 | let didSetImage = self.hnk_fetchImageForFetcher(fetcher, state: state, format : format, failure: fail, success: succeed) 44 | 45 | if didSetImage { return } 46 | 47 | if let placeholder = placeholder { 48 | self.setImage(placeholder, for: state) 49 | } 50 | } 51 | 52 | func hnk_cancelSetImage() { 53 | if let fetcher = self.hnk_imageFetcher { 54 | fetcher.cancelFetch() 55 | self.hnk_imageFetcher = nil 56 | } 57 | } 58 | 59 | // MARK: Internal Image 60 | 61 | // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances 62 | var hnk_imageFetcher : Fetcher<UIImage>! { 63 | get { 64 | let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey) as? ObjectWrapper 65 | let fetcher = wrapper?.hnk_value as? Fetcher<UIImage> 66 | return fetcher 67 | } 68 | set (fetcher) { 69 | var wrapper : ObjectWrapper? 70 | if let fetcher = fetcher { 71 | wrapper = ObjectWrapper(value: fetcher) 72 | } 73 | objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 74 | } 75 | } 76 | 77 | func hnk_fetchImageForFetcher(_ fetcher : Fetcher<UIImage>, state : UIControl.State = .normal, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool { 78 | let format = format ?? self.hnk_imageFormat 79 | let cache = Shared.imageCache 80 | if cache.formats[format.name] == nil { 81 | cache.addFormat(format) 82 | } 83 | var animated = false 84 | let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in 85 | if let strongSelf = self { 86 | if strongSelf.hnk_shouldCancelImageForKey(fetcher.key) { return } 87 | 88 | strongSelf.hnk_imageFetcher = nil 89 | 90 | fail?(error) 91 | } 92 | }) { [weak self] image in 93 | if let strongSelf = self { 94 | if strongSelf.hnk_shouldCancelImageForKey(fetcher.key) { return } 95 | 96 | strongSelf.hnk_setImage(image, state: state, animated: animated, success: succeed) 97 | } 98 | } 99 | animated = true 100 | return fetch.hasSucceeded 101 | } 102 | 103 | 104 | func hnk_setImage(_ image : UIImage, state : UIControl.State, animated : Bool, success succeed : ((UIImage) -> ())?) { 105 | self.hnk_imageFetcher = nil 106 | 107 | if let succeed = succeed { 108 | succeed(image) 109 | } else if animated { 110 | UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: { 111 | self.setImage(image, for: state) 112 | }, completion: nil) 113 | } else { 114 | self.setImage(image, for: state) 115 | } 116 | } 117 | 118 | func hnk_shouldCancelImageForKey(_ key:String) -> Bool { 119 | if self.hnk_imageFetcher?.key == key { return false } 120 | 121 | Log.debug(message: "Cancelled set image for \((key as NSString).lastPathComponent)") 122 | return true 123 | } 124 | 125 | // MARK: Background image 126 | 127 | var hnk_backgroundImageFormat : Format<UIImage> { 128 | let bounds = self.bounds 129 | assert(bounds.size.width > 0 && bounds.size.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UIButton size is zero. Set its frame, call sizeToFit or force layout first. You can also set a custom format with a defined size if you don't want to force layout.") 130 | let imageSize = self.backgroundRect(forBounds: bounds).size 131 | 132 | return HanekeGlobals.UIKit.formatWithSize(imageSize, scaleMode: .Fill) 133 | } 134 | 135 | func hnk_setBackgroundImageFromURL(_ URL : Foundation.URL, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) { 136 | let fetcher = NetworkFetcher<UIImage>(URL: URL) 137 | self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed) 138 | } 139 | 140 | func hnk_setBackgroundImage(_ image : UIImage, key: String, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, success succeed : ((UIImage) -> ())? = nil) { 141 | let fetcher = SimpleFetcher<UIImage>(key: key, value: image) 142 | self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, success: succeed) 143 | } 144 | 145 | func hnk_setBackgroundImageFromFile(_ path: String, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) { 146 | let fetcher = DiskFetcher<UIImage>(path: path) 147 | self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed) 148 | } 149 | 150 | func hnk_setBackgroundImageFromFetcher(_ fetcher : Fetcher<UIImage>, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) { 151 | self.hnk_cancelSetBackgroundImage() 152 | self.hnk_backgroundImageFetcher = fetcher 153 | 154 | let didSetImage = self.hnk_fetchBackgroundImageForFetcher(fetcher, state: state, format : format, failure: fail, success: succeed) 155 | 156 | if didSetImage { return } 157 | 158 | if let placeholder = placeholder { 159 | self.setBackgroundImage(placeholder, for: state) 160 | } 161 | } 162 | 163 | func hnk_cancelSetBackgroundImage() { 164 | if let fetcher = self.hnk_backgroundImageFetcher { 165 | fetcher.cancelFetch() 166 | self.hnk_backgroundImageFetcher = nil 167 | } 168 | } 169 | 170 | // MARK: Internal Background image 171 | 172 | // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances 173 | var hnk_backgroundImageFetcher : Fetcher<UIImage>! { 174 | get { 175 | let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetBackgroundImageFetcherKey) as? ObjectWrapper 176 | let fetcher = wrapper?.hnk_value as? Fetcher<UIImage> 177 | return fetcher 178 | } 179 | set (fetcher) { 180 | var wrapper : ObjectWrapper? 181 | if let fetcher = fetcher { 182 | wrapper = ObjectWrapper(value: fetcher) 183 | } 184 | objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetBackgroundImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 185 | } 186 | } 187 | 188 | func hnk_fetchBackgroundImageForFetcher(_ fetcher: Fetcher<UIImage>, state: UIControl.State = .normal, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool { 189 | let format = format ?? self.hnk_backgroundImageFormat 190 | let cache = Shared.imageCache 191 | if cache.formats[format.name] == nil { 192 | cache.addFormat(format) 193 | } 194 | var animated = false 195 | let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in 196 | if let strongSelf = self { 197 | if strongSelf.hnk_shouldCancelBackgroundImageForKey(fetcher.key) { return } 198 | 199 | strongSelf.hnk_backgroundImageFetcher = nil 200 | 201 | fail?(error) 202 | } 203 | }) { [weak self] image in 204 | if let strongSelf = self { 205 | if strongSelf.hnk_shouldCancelBackgroundImageForKey(fetcher.key) { return } 206 | 207 | strongSelf.hnk_setBackgroundImage(image, state: state, animated: animated, success: succeed) 208 | } 209 | } 210 | animated = true 211 | return fetch.hasSucceeded 212 | } 213 | 214 | func hnk_setBackgroundImage(_ image: UIImage, state: UIControl.State, animated: Bool, success succeed: ((UIImage) -> ())?) { 215 | self.hnk_backgroundImageFetcher = nil 216 | 217 | if let succeed = succeed { 218 | succeed(image) 219 | } else if animated { 220 | UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: { 221 | self.setBackgroundImage(image, for: state) 222 | }, completion: nil) 223 | } else { 224 | self.setBackgroundImage(image, for: state) 225 | } 226 | } 227 | 228 | func hnk_shouldCancelBackgroundImageForKey(_ key: String) -> Bool { 229 | if self.hnk_backgroundImageFetcher?.key == key { return false } 230 | 231 | Log.debug(message: "Cancelled set background image for \((key as NSString).lastPathComponent)") 232 | return true 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Haneke/UIImage+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | func hnk_imageByScaling(toSize size: CGSize) -> UIImage { 14 | UIGraphicsBeginImageContextWithOptions(size, !hnk_hasAlpha(), 0.0) 15 | draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) 16 | let resizedImage = UIGraphicsGetImageFromCurrentImageContext() 17 | UIGraphicsEndImageContext() 18 | return resizedImage! 19 | } 20 | 21 | func hnk_hasAlpha() -> Bool { 22 | guard let alphaInfo = self.cgImage?.alphaInfo else { return false } 23 | switch alphaInfo { 24 | case .first, .last, .premultipliedFirst, .premultipliedLast, .alphaOnly: 25 | return true 26 | case .none, .noneSkipFirst, .noneSkipLast: 27 | return false 28 | @unknown default: 29 | fatalError() 30 | } 31 | } 32 | 33 | func hnk_data(compressionQuality: Float = 1.0) -> Data! { 34 | let hasAlpha = self.hnk_hasAlpha() 35 | let data = hasAlpha ? self.pngData() : self.jpegData(compressionQuality: CGFloat(compressionQuality)) 36 | return data 37 | } 38 | 39 | func hnk_decompressedImage() -> UIImage! { 40 | let originalImageRef = self.cgImage 41 | let originalBitmapInfo = originalImageRef?.bitmapInfo 42 | guard let alphaInfo = originalImageRef?.alphaInfo else { return UIImage() } 43 | 44 | // See: http://stackoverflow.com/questions/23723564/which-cgimagealphainfo-should-we-use 45 | var bitmapInfo = originalBitmapInfo 46 | switch alphaInfo { 47 | case .none: 48 | let rawBitmapInfoWithoutAlpha = (bitmapInfo?.rawValue)! & ~CGBitmapInfo.alphaInfoMask.rawValue 49 | let rawBitmapInfo = rawBitmapInfoWithoutAlpha | CGImageAlphaInfo.noneSkipFirst.rawValue 50 | bitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo) 51 | case .premultipliedFirst, .premultipliedLast, .noneSkipFirst, .noneSkipLast: 52 | break 53 | case .alphaOnly, .last, .first: // Unsupported 54 | return self 55 | @unknown default: 56 | fatalError() 57 | } 58 | 59 | let colorSpace = CGColorSpaceCreateDeviceRGB() 60 | let pixelSize = CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale) 61 | guard let context = CGContext(data: nil, width: Int(ceil(pixelSize.width)), height: Int(ceil(pixelSize.height)), bitsPerComponent: (originalImageRef?.bitsPerComponent)!, bytesPerRow: 0, space: colorSpace, bitmapInfo: (bitmapInfo?.rawValue)!) else { 62 | return self 63 | } 64 | 65 | let imageRect = CGRect(x: 0, y: 0, width: pixelSize.width, height: pixelSize.height) 66 | UIGraphicsPushContext(context) 67 | 68 | // Flip coordinate system. See: http://stackoverflow.com/questions/506622/cgcontextdrawimage-draws-image-upside-down-when-passed-uiimage-cgimage 69 | context.translateBy(x: 0, y: pixelSize.height) 70 | context.scaleBy(x: 1.0, y: -1.0) 71 | 72 | // UIImage and drawInRect takes into account image orientation, unlike CGContextDrawImage. 73 | self.draw(in: imageRect) 74 | UIGraphicsPopContext() 75 | 76 | guard let decompressedImageRef = context.makeImage() else { 77 | return self 78 | } 79 | 80 | let scale = UIScreen.main.scale 81 | let image = UIImage(cgImage: decompressedImageRef, scale:scale, orientation:UIImage.Orientation.up) 82 | return image 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Haneke/UIImageView+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/17/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIImageView { 12 | 13 | var hnk_format : Format<UIImage> { 14 | let viewSize = self.bounds.size 15 | assert(viewSize.width > 0 && viewSize.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UImageView size is zero. Set its frame, call sizeToFit or force layout first.") 16 | let scaleMode = self.hnk_scaleMode 17 | return HanekeGlobals.UIKit.formatWithSize(viewSize, scaleMode: scaleMode) 18 | } 19 | 20 | func hnk_setImageFromURL(_ URL: Foundation.URL, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) { 21 | let fetcher = NetworkFetcher<UIImage>(URL: URL) 22 | self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, failure: fail, success: succeed) 23 | } 24 | 25 | func hnk_setImage( _ image: @autoclosure @escaping () -> UIImage, key: String, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, success succeed : ((UIImage) -> ())? = nil) { 26 | let fetcher = SimpleFetcher<UIImage>(key: key, value: image()) 27 | self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, success: succeed) 28 | } 29 | 30 | func hnk_setImageFromFile(_ path: String, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) { 31 | let fetcher = DiskFetcher<UIImage>(path: path) 32 | self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, failure: fail, success: succeed) 33 | } 34 | 35 | func hnk_setImage(fromFetcher fetcher : Fetcher<UIImage>, 36 | placeholder : UIImage? = nil, 37 | format : Format<UIImage>? = nil, 38 | failure fail : ((Error?) -> ())? = nil, 39 | success succeed : ((UIImage) -> ())? = nil) { 40 | 41 | self.hnk_cancelSetImage() 42 | 43 | self.hnk_fetcher = fetcher 44 | 45 | let didSetImage = self.hnk_fetchImageForFetcher(fetcher, format: format, failure: fail, success: succeed) 46 | 47 | if didSetImage { return } 48 | 49 | if let placeholder = placeholder { 50 | self.image = placeholder 51 | } 52 | } 53 | 54 | func hnk_cancelSetImage() { 55 | if let fetcher = self.hnk_fetcher { 56 | fetcher.cancelFetch() 57 | self.hnk_fetcher = nil 58 | } 59 | } 60 | 61 | // MARK: Internal 62 | 63 | // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances 64 | var hnk_fetcher : Fetcher<UIImage>! { 65 | get { 66 | let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey) as? ObjectWrapper 67 | let fetcher = wrapper?.hnk_value as? Fetcher<UIImage> 68 | return fetcher 69 | } 70 | set (fetcher) { 71 | var wrapper : ObjectWrapper? 72 | if let fetcher = fetcher { 73 | wrapper = ObjectWrapper(value: fetcher) 74 | } 75 | objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) 76 | } 77 | } 78 | 79 | var hnk_scaleMode : ImageResizer.ScaleMode { 80 | switch (self.contentMode) { 81 | case .scaleToFill: 82 | return .Fill 83 | case .scaleAspectFit: 84 | return .AspectFit 85 | case .scaleAspectFill: 86 | return .AspectFill 87 | case .redraw, .center, .top, .bottom, .left, .right, .topLeft, .topRight, .bottomLeft, .bottomRight: 88 | return .None 89 | @unknown default: 90 | fatalError() 91 | } 92 | } 93 | 94 | func hnk_fetchImageForFetcher(_ fetcher : Fetcher<UIImage>, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool { 95 | let cache = Shared.imageCache 96 | let format = format ?? self.hnk_format 97 | if cache.formats[format.name] == nil { 98 | cache.addFormat(format) 99 | } 100 | var animated = false 101 | let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in 102 | if let strongSelf = self { 103 | if strongSelf.hnk_shouldCancel(forKey: fetcher.key) { return } 104 | 105 | strongSelf.hnk_fetcher = nil 106 | 107 | fail?(error) 108 | } 109 | }) { [weak self] image in 110 | if let strongSelf = self { 111 | if strongSelf.hnk_shouldCancel(forKey: fetcher.key) { return } 112 | 113 | strongSelf.hnk_setImage(image, animated: animated, success: succeed) 114 | } 115 | } 116 | animated = true 117 | return fetch.hasSucceeded 118 | } 119 | 120 | func hnk_setImage(_ image : UIImage, animated : Bool, success succeed : ((UIImage) -> ())?) { 121 | self.hnk_fetcher = nil 122 | 123 | if let succeed = succeed { 124 | succeed(image) 125 | } else if animated { 126 | UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: { 127 | self.image = image 128 | }, completion: nil) 129 | } else { 130 | self.image = image 131 | } 132 | } 133 | 134 | func hnk_shouldCancel(forKey key:String) -> Bool { 135 | if self.hnk_fetcher?.key == key { return false } 136 | 137 | Log.debug(message: "Cancelled set image for \((key as NSString).lastPathComponent)") 138 | return true 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Haneke/UIView+Haneke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Haneke.swift 3 | // Haneke 4 | // 5 | // Created by Joan Romano on 15/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension HanekeGlobals { 12 | 13 | struct UIKit { 14 | 15 | static func formatWithSize(_ size : CGSize, scaleMode : ImageResizer.ScaleMode, allowUpscaling: Bool = true) -> Format<UIImage> { 16 | let name = "auto-\(size.width)x\(size.height)-\(scaleMode.rawValue)" 17 | let cache = Shared.imageCache 18 | if let (format,_,_) = cache.formats[name] { 19 | return format 20 | } 21 | 22 | var format = Format<UIImage>(name: name, 23 | diskCapacity: HanekeGlobals.UIKit.DefaultFormat.DiskCapacity) { 24 | let resizer = ImageResizer(size:size, 25 | scaleMode: scaleMode, 26 | allowUpscaling: allowUpscaling, 27 | compressionQuality: HanekeGlobals.UIKit.DefaultFormat.CompressionQuality) 28 | return resizer.resizeImage($0) 29 | } 30 | format.convertToData = {(image : UIImage) -> Data in 31 | image.hnk_data(compressionQuality: HanekeGlobals.UIKit.DefaultFormat.CompressionQuality) as Data 32 | } 33 | return format 34 | } 35 | 36 | public struct DefaultFormat { 37 | 38 | public static let DiskCapacity : UInt64 = 50 * 1024 * 1024 39 | public static let CompressionQuality : Float = 0.75 40 | 41 | } 42 | 43 | static var SetImageAnimationDuration = 0.1 44 | static var SetImageFetcherKey = 0 45 | static var SetBackgroundImageFetcherKey = 1 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /HanekeDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // HanekeDemo 4 | // 5 | // Created by Hermes Pique on 9/17/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /HanekeDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6245" systemVersion="13E28" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES"> 3 | <dependencies> 4 | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/> 5 | <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/> 6 | </dependencies> 7 | <objects> 8 | <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> 9 | <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> 10 | <view contentMode="scaleToFill" id="iN0-l3-epB"> 11 | <rect key="frame" x="0.0" y="0.0" width="480" height="480"/> 12 | <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> 13 | <subviews> 14 | <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2014 Haneke. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye"> 15 | <rect key="frame" x="20" y="439" width="441" height="21"/> 16 | <fontDescription key="fontDescription" type="system" pointSize="17"/> 17 | <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> 18 | <nil key="highlightedColor"/> 19 | </label> 20 | <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Haneke Demo" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX"> 21 | <rect key="frame" x="20" y="140" width="441" height="43"/> 22 | <fontDescription key="fontDescription" type="boldSystem" pointSize="36"/> 23 | <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> 24 | <nil key="highlightedColor"/> 25 | </label> 26 | </subviews> 27 | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> 28 | <constraints> 29 | <constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC"/> 30 | <constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk"/> 31 | <constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l"/> 32 | <constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0"/> 33 | <constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/> 34 | <constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g"/> 35 | </constraints> 36 | <nil key="simulatedStatusBarMetrics"/> 37 | <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/> 38 | <point key="canvasLocation" x="548" y="455"/> 39 | </view> 40 | </objects> 41 | </document> 42 | -------------------------------------------------------------------------------- /HanekeDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6245" systemVersion="13E28" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r"> 3 | <dependencies> 4 | <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/> 5 | </dependencies> 6 | <scenes> 7 | <!--View Controller--> 8 | <scene sceneID="tne-QT-ifu"> 9 | <objects> 10 | <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="HanekeDemo" customModuleProvider="target" sceneMemberID="viewController"> 11 | <layoutGuides> 12 | <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/> 13 | <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/> 14 | </layoutGuides> 15 | <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC" customClass="UICollectionView"> 16 | <rect key="frame" x="0.0" y="0.0" width="600" height="600"/> 17 | <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> 18 | <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> 19 | </view> 20 | </viewController> 21 | <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> 22 | </objects> 23 | </scene> 24 | </scenes> 25 | </document> 26 | -------------------------------------------------------------------------------- /HanekeDemo/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/17/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Haneke 11 | 12 | class CollectionViewCell: UICollectionViewCell { 13 | 14 | var imageView : UIImageView! 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | initHelper() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | initHelper() 24 | } 25 | 26 | func initHelper() { 27 | imageView = UIImageView(frame: self.contentView.bounds) 28 | imageView.clipsToBounds = true 29 | imageView.contentMode = .scaleAspectFill 30 | imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 31 | self.contentView.addSubview(imageView) 32 | } 33 | 34 | override func prepareForReuse() { 35 | imageView.hnk_cancelSetImage() 36 | imageView.image = nil 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /HanekeDemo/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 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "icon-60@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon-60@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "size" : "20x20", 48 | "scale" : "1x" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "size" : "20x20", 53 | "scale" : "2x" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "size" : "29x29", 58 | "scale" : "1x" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "size" : "29x29", 63 | "scale" : "2x" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "size" : "40x40", 68 | "scale" : "1x" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "size" : "40x40", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "76x76", 77 | "idiom" : "ipad", 78 | "filename" : "icon-76.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "76x76", 83 | "idiom" : "ipad", 84 | "filename" : "icon-76@2x.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "idiom" : "ipad", 89 | "size" : "83.5x83.5", 90 | "scale" : "2x" 91 | } 92 | ], 93 | "info" : { 94 | "version" : 1, 95 | "author" : "xcode" 96 | } 97 | } -------------------------------------------------------------------------------- /HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haneke/HanekeSwift/a2e8e5b9a91eef90138a4f43c9a0044c4e90a6ef/HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haneke/HanekeSwift/a2e8e5b9a91eef90138a4f43c9a0044c4e90a6ef/HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haneke/HanekeSwift/a2e8e5b9a91eef90138a4f43c9a0044c4e90a6ef/HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haneke/HanekeSwift/a2e8e5b9a91eef90138a4f43c9a0044c4e90a6ef/HanekeDemo/Images.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /HanekeDemo/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>$(EXECUTABLE_NAME)</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>Haneke</string> 15 | <key>CFBundlePackageType</key> 16 | <string>APPL</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.1</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>1</string> 23 | <key>LSRequiresIPhoneOS</key> 24 | <true/> 25 | <key>NSAppTransportSecurity</key> 26 | <dict> 27 | <key>NSAllowsArbitraryLoads</key> 28 | <true/> 29 | </dict> 30 | <key>UILaunchStoryboardName</key> 31 | <string>LaunchScreen</string> 32 | <key>UIMainStoryboardFile</key> 33 | <string>Main</string> 34 | <key>UIRequiredDeviceCapabilities</key> 35 | <array> 36 | <string>armv7</string> 37 | </array> 38 | <key>UISupportedInterfaceOrientations</key> 39 | <array> 40 | <string>UIInterfaceOrientationPortrait</string> 41 | <string>UIInterfaceOrientationLandscapeLeft</string> 42 | <string>UIInterfaceOrientationLandscapeRight</string> 43 | </array> 44 | <key>UISupportedInterfaceOrientations~ipad</key> 45 | <array> 46 | <string>UIInterfaceOrientationPortrait</string> 47 | <string>UIInterfaceOrientationPortraitUpsideDown</string> 48 | <string>UIInterfaceOrientationLandscapeLeft</string> 49 | <string>UIInterfaceOrientationLandscapeRight</string> 50 | </array> 51 | </dict> 52 | </plist> 53 | -------------------------------------------------------------------------------- /HanekeDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // HanekeDemo 4 | // 5 | // Created by Hermes Pique on 9/17/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let CellReuseIdentifier = "Cell" 12 | 13 | class ViewController: UICollectionViewController { 14 | 15 | var items : [String] = [] 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | self.collectionView!.register(CollectionViewCell.self, forCellWithReuseIdentifier: CellReuseIdentifier) 20 | let layout = UICollectionViewFlowLayout() 21 | layout.itemSize = CGSize(width: 100, height: 100) 22 | self.collectionView!.collectionViewLayout = layout 23 | 24 | self.initializeItemsWithURLs() 25 | } 26 | 27 | // MARK: UIViewCollectionViewDataSource 28 | 29 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 30 | return items.count 31 | } 32 | 33 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 34 | let CellIdentifier = "Cell" 35 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellIdentifier, for: indexPath) as! CollectionViewCell 36 | let URLString = self.items[(indexPath as NSIndexPath).row] 37 | let url = URL(string:URLString)! 38 | cell.imageView.hnk_setImageFromURL(url) 39 | return cell 40 | } 41 | 42 | // MARK: Helpers 43 | 44 | func initializeItemsWithURLs() { 45 | items = ["http://imgs.xkcd.com/comics/election.png", 46 | "http://imgs.xkcd.com/comics/scantron.png", 47 | "http://imgs.xkcd.com/comics/secretary_part_5.png", 48 | "http://imgs.xkcd.com/comics/secretary_part_4.png", 49 | "http://imgs.xkcd.com/comics/secretary_part_3.png", 50 | "http://imgs.xkcd.com/comics/secretary_part_2.png", 51 | "http://imgs.xkcd.com/comics/secretary_part_1.png", 52 | "http://imgs.xkcd.com/comics/actuarial.png", 53 | "http://imgs.xkcd.com/comics/scrabble.png", 54 | "http://imgs.xkcd.com/comics/twitter.png", 55 | "http://imgs.xkcd.com/comics/morning_routine.png", 56 | "http://imgs.xkcd.com/comics/going_west.png", 57 | "http://imgs.xkcd.com/comics/steal_this_comic.png", 58 | "http://imgs.xkcd.com/comics/numerical_sex_positions.png", 59 | "http://imgs.xkcd.com/comics/i_am_not_a_ninja.png", 60 | "http://imgs.xkcd.com/comics/depth.png", 61 | "http://imgs.xkcd.com/comics/flash_games.png", 62 | "http://imgs.xkcd.com/comics/fiction_rule_of_thumb.png", 63 | "http://imgs.xkcd.com/comics/height.png", 64 | "http://imgs.xkcd.com/comics/listen_to_yourself.png", 65 | "http://imgs.xkcd.com/comics/spore.png", 66 | "http://imgs.xkcd.com/comics/tones.png", 67 | "http://imgs.xkcd.com/comics/the_staple_madness.png", 68 | "http://imgs.xkcd.com/comics/typewriter.png", 69 | "http://imgs.xkcd.com/comics/one-sided.png", 70 | "http://imgs.xkcd.com/comics/further_boomerang_difficulties.png", 71 | "http://imgs.xkcd.com/comics/turn-on.png", 72 | "http://imgs.xkcd.com/comics/still_raw.png", 73 | "http://imgs.xkcd.com/comics/house_of_pancakes.png", 74 | "http://imgs.xkcd.com/comics/aversion_fads.png", 75 | "http://imgs.xkcd.com/comics/the_end_is_not_for_a_while.png", 76 | "http://imgs.xkcd.com/comics/improvised.png", 77 | "http://imgs.xkcd.com/comics/fetishes.png", 78 | "http://imgs.xkcd.com/comics/x_girls_y_cups.png", 79 | "http://imgs.xkcd.com/comics/moving.png", 80 | "http://imgs.xkcd.com/comics/quantum_teleportation.png", 81 | "http://imgs.xkcd.com/comics/rba.png", 82 | "http://imgs.xkcd.com/comics/voting_machines.png", 83 | "http://imgs.xkcd.com/comics/freemanic_paracusia.png", 84 | "http://imgs.xkcd.com/comics/google_maps.png", 85 | "http://imgs.xkcd.com/comics/paleontology.png", 86 | "http://imgs.xkcd.com/comics/holy_ghost.png", 87 | "http://imgs.xkcd.com/comics/regrets.png", 88 | "http://imgs.xkcd.com/comics/frustration.png", 89 | "http://imgs.xkcd.com/comics/cautionary.png", 90 | "http://imgs.xkcd.com/comics/hats.png", 91 | "http://imgs.xkcd.com/comics/rewiring.png", 92 | "http://imgs.xkcd.com/comics/upcoming_hurricanes.png", 93 | "http://imgs.xkcd.com/comics/mission.png", 94 | "http://imgs.xkcd.com/comics/impostor.png", 95 | "http://imgs.xkcd.com/comics/the_sea.png", 96 | "http://imgs.xkcd.com/comics/things_fall_apart.png", 97 | "http://imgs.xkcd.com/comics/good_morning.png", 98 | "http://imgs.xkcd.com/comics/too_old_for_this_shit.png", 99 | "http://imgs.xkcd.com/comics/in_popular_culture.png", 100 | "http://imgs.xkcd.com/comics/i_am_not_good_with_boomerangs.png", 101 | "http://imgs.xkcd.com/comics/macgyver_gets_lazy.png", 102 | "http://imgs.xkcd.com/comics/know_your_vines.png", 103 | "http://imgs.xkcd.com/comics/xkcd_loves_the_discovery_channel.png", 104 | "http://imgs.xkcd.com/comics/babies.png", 105 | "http://imgs.xkcd.com/comics/road_rage.png", 106 | "http://imgs.xkcd.com/comics/thinking_ahead.png", 107 | "http://imgs.xkcd.com/comics/internet_argument.png", 108 | "http://imgs.xkcd.com/comics/suv.png", 109 | "http://imgs.xkcd.com/comics/how_it_happened.png", 110 | "http://imgs.xkcd.com/comics/purity.png", 111 | "http://imgs.xkcd.com/comics/xkcd_goes_to_the_airport.png", 112 | "http://imgs.xkcd.com/comics/journal_5.png", 113 | "http://imgs.xkcd.com/comics/journal_4.png", 114 | "http://imgs.xkcd.com/comics/delivery.png", 115 | "http://imgs.xkcd.com/comics/every_damn_morning.png", 116 | "http://imgs.xkcd.com/comics/fantasy.png", 117 | "http://imgs.xkcd.com/comics/starwatching.png", 118 | "http://imgs.xkcd.com/comics/bad_timing.png", 119 | "http://imgs.xkcd.com/comics/geohashing.png", 120 | "http://imgs.xkcd.com/comics/fortune_cookies.png", 121 | "http://imgs.xkcd.com/comics/security_holes.png", 122 | "http://imgs.xkcd.com/comics/finish_line.png", 123 | "http://imgs.xkcd.com/comics/a_better_idea.png", 124 | "http://imgs.xkcd.com/comics/making_hash_browns.png", 125 | "http://imgs.xkcd.com/comics/jealousy.png", 126 | "http://imgs.xkcd.com/comics/forks_and_spoons.png", 127 | "http://imgs.xkcd.com/comics/stove_ownership.png", 128 | "http://imgs.xkcd.com/comics/the_man_who_fell_sideways.png", 129 | "http://imgs.xkcd.com/comics/zealous_autoconfig.png", 130 | "http://imgs.xkcd.com/comics/restraining_order.png", 131 | "http://imgs.xkcd.com/comics/mistranslations.png", 132 | "http://imgs.xkcd.com/comics/new_pet.png", 133 | "http://imgs.xkcd.com/comics/startled.png", 134 | "http://imgs.xkcd.com/comics/techno.png", 135 | "http://imgs.xkcd.com/comics/math_paper.png", 136 | "http://imgs.xkcd.com/comics/electric_skateboard_double_comic.png", 137 | "http://imgs.xkcd.com/comics/overqualified.png", 138 | "http://imgs.xkcd.com/comics/cheap_gps.png", 139 | "http://imgs.xkcd.com/comics/venting.png", 140 | "http://imgs.xkcd.com/comics/journal_3.png", 141 | "http://imgs.xkcd.com/comics/convincing_pickup_line.png", 142 | "http://imgs.xkcd.com/comics/1000_miles_north.png", 143 | "http://imgs.xkcd.com/comics/large_hadron_collider.png", 144 | "http://imgs.xkcd.com/comics/important_life_lesson.png"] 145 | } 146 | 147 | } 148 | 149 | -------------------------------------------------------------------------------- /HanekeSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'HanekeSwift' 3 | s.module_name = 'Haneke' 4 | s.version = '1.2' 5 | s.license = 'Apache' 6 | s.summary = 'A lightweight generic cache for iOS written in Swift with extra love for images.' 7 | s.homepage = 'https://github.com/Haneke/HanekeSwift' 8 | s.authors = { 'Hermes Pique' => 'https://twitter.com/hpique' } 9 | s.source = { :git => 'https://github.com/Haneke/HanekeSwift.git', :tag => "v#{s.version}" } 10 | s.swift_version = '5.0' 11 | s.tvos.deployment_target = '9.1' 12 | s.ios.deployment_target = '8.0' 13 | s.source_files = 'Haneke/*.swift' 14 | end 15 | -------------------------------------------------------------------------------- /HanekeTests/AsyncFetcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncFetcher.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 1/2/16. 6 | // Copyright © 2016 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Haneke 11 | 12 | class AsyncFetcher<T : DataConvertible> : Fetcher<T> { 13 | 14 | let getValue : () -> T.Result 15 | 16 | init(key: String, value getValue : @autoclosure @escaping () -> T.Result) { 17 | self.getValue = getValue 18 | super.init(key: key) 19 | } 20 | 21 | override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) { 22 | let value = getValue() 23 | DispatchQueue.global(qos: .default).async { 24 | DispatchQueue.main.async { 25 | succeed(value) 26 | } 27 | } 28 | } 29 | 30 | override func cancelFetch() {} 31 | 32 | } 33 | -------------------------------------------------------------------------------- /HanekeTests/CGSize+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Oriol Blanc Gimeno on 9/12/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class CGSize_HanekeTests: XCTestCase { 14 | 15 | func testAspectFillSize() { 16 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 1), false) 17 | let sut: CGSize = image.size.hnk_aspectFillSize(CGSize(width: 10, height: 10)) 18 | 19 | XCTAssertTrue(sut.height == 10) 20 | } 21 | 22 | func testAspectFitSize() { 23 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 1), false) 24 | let sut: CGSize = image.size.hnk_aspectFitSize(CGSize(width: 20, height: 20)) 25 | 26 | XCTAssertTrue(sut.height == 2) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /HanekeTests/DataTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/19/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class ImageDataTests: XCTestCase { 14 | 15 | func testConvertFromData() { 16 | let image = UIImage.imageGradientFromColor() 17 | let data = image.hnk_data() 18 | 19 | let result = UIImage.convertFromData(data!) 20 | 21 | XCTAssertTrue(image.isEqualPixelByPixel(result!)) 22 | } 23 | 24 | func testAsData() { 25 | let image = UIImage.imageGradientFromColor() 26 | let data = image.hnk_data() 27 | 28 | let result = image.asData() 29 | 30 | XCTAssertEqual(result, data) 31 | } 32 | 33 | } 34 | 35 | class StringDataTests: XCTestCase { 36 | 37 | func testConvertFromData() { 38 | let string = self.name 39 | let data = string.data(using: String.Encoding.utf8)! 40 | 41 | let result = String.convertFromData(data) 42 | 43 | XCTAssertEqual(result!, string) 44 | } 45 | 46 | func testAsData() { 47 | let string = self.name 48 | let data = string.data(using: String.Encoding.utf8)! 49 | 50 | let result = string.asData() 51 | 52 | XCTAssertEqual(result, data) 53 | } 54 | 55 | } 56 | 57 | class DataDataTests: XCTestCase { 58 | 59 | func testConvertFromData() { 60 | let data = Data.dataWithLength(32) 61 | 62 | let result = Data.convertFromData(data) 63 | 64 | XCTAssertEqual(result!, data) 65 | } 66 | 67 | func testAsData() { 68 | let data = Data.dataWithLength(32) 69 | 70 | let result = data.asData() 71 | 72 | XCTAssertEqual(result, data) 73 | } 74 | 75 | } 76 | 77 | class JSONDataTests: XCTestCase { 78 | 79 | func testConvertFromData_WithArrayData() { 80 | let json = [self.name] 81 | let data = try! JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions()) 82 | 83 | let result = JSON.convertFromData(data)! 84 | 85 | switch result { 86 | case .Dictionary(_): 87 | XCTFail("expected array") 88 | case .Array(let object): 89 | let resultData = try! JSONSerialization.data(withJSONObject: object, options: JSONSerialization.WritingOptions()) 90 | XCTAssertEqual(resultData, data) 91 | } 92 | } 93 | 94 | func testConvertFromData_WithDictionaryData() { 95 | let json = ["test": self.name] 96 | let data = try! JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions()) 97 | 98 | let result = JSON.convertFromData(data)! 99 | 100 | switch result { 101 | case .Dictionary(let object): 102 | try! JSONSerialization.data(withJSONObject: object, options: JSONSerialization.WritingOptions()) 103 | case .Array(_): 104 | XCTFail("expected dictionary") 105 | } 106 | } 107 | 108 | func testConvertFromData_WithInvalidData() { 109 | let data = Data.dataWithLength(100) 110 | 111 | let result = JSON.convertFromData(data) 112 | 113 | XCTAssertTrue(result == nil) 114 | } 115 | 116 | func testAsData_Array() { 117 | let object = [self.name] 118 | let json = JSON.Array(object as [AnyObject]) 119 | 120 | let result = json.asData() 121 | 122 | let data = try! JSONSerialization.data(withJSONObject: object, options: JSONSerialization.WritingOptions()) 123 | XCTAssertEqual(result, data) 124 | } 125 | 126 | func testAsData_Dictionary() { 127 | let object = ["test": self.name] 128 | let json = JSON.Dictionary(object as [String : AnyObject]) 129 | 130 | let result = json.asData() 131 | 132 | let data = try! JSONSerialization.data(withJSONObject: object, options: JSONSerialization.WritingOptions()) 133 | XCTAssertEqual(result, data) 134 | } 135 | 136 | func testAsData_InvalidJSON() { 137 | // TODO: Swift doesn't support XCAssertThrows yet. 138 | // See: http://stackoverflow.com/questions/25529625/testing-assertion-in-swift 139 | 140 | // let object = ["test": UIImage.imageWithColor(UIColor.redColor())] 141 | // let json = JSON.Dictionary(object) 142 | // XCAssertThrows(json.asData()) 143 | } 144 | 145 | func testArray_Array() { 146 | let object = [self.name] 147 | let json = JSON.Array(object as [AnyObject]) 148 | 149 | let result = json.array 150 | 151 | XCTAssertNotNil(result) 152 | } 153 | 154 | func testArray_Dictionary() { 155 | let object = ["test": self.name] 156 | let json = JSON.Dictionary(object as [String : AnyObject]) 157 | 158 | let result = json.array 159 | 160 | XCTAssertNil(result) 161 | } 162 | 163 | func testDictionary_Array() { 164 | let object = [self.name] 165 | let json = JSON.Array(object as [AnyObject]) 166 | 167 | let result = json.dictionary 168 | 169 | XCTAssertNil(result) 170 | } 171 | 172 | func testDictionary_Dictionary() { 173 | let object = ["test": self.name] 174 | let json = JSON.Dictionary(object as [String : AnyObject]) 175 | 176 | let result = json.dictionary 177 | 178 | XCTAssertNotNil(result) 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /HanekeTests/DiskFetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskFetcherTests.swift 3 | // Haneke 4 | // 5 | // Created by Joan Romano on 21/09/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class DiskFetcherTests: DiskTestCase { 14 | 15 | var sut : DiskFetcher<UIImage>! 16 | var path: String! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | path = self.uniquePath() 21 | sut = DiskFetcher(path: path) 22 | } 23 | 24 | func testInit() { 25 | XCTAssertEqual(sut.path, path) 26 | } 27 | 28 | func testKey() { 29 | XCTAssertEqual(sut.key, path) 30 | } 31 | 32 | func testFetchImage_Success() { 33 | let image = UIImage.imageWithColor(UIColor.green, CGSize(width: 10, height: 20)) 34 | let data = image.pngData()! 35 | try? data.write(to: URL(fileURLWithPath: sut.path), options: [.atomic]) 36 | 37 | let expectation = self.expectation(description: self.name) 38 | 39 | sut.fetch(failure: { _ in 40 | XCTFail("Expected to succeed") 41 | expectation.fulfill() 42 | }) { 43 | let result = $0 as UIImage 44 | XCTAssertTrue(result.isEqualPixelByPixel(image)) 45 | expectation.fulfill() 46 | } 47 | 48 | self.waitForExpectations(timeout: 1, handler: nil) 49 | } 50 | 51 | func testFetchImage_Failure_NSFileReadNoSuchFileError() { 52 | let expectation = self.expectation(description: self.name) 53 | 54 | sut.fetch(failure: { 55 | guard let error = $0 as NSError? else { 56 | XCTFail("expected non-nil error"); 57 | expectation.fulfill() 58 | return 59 | } 60 | XCTAssertEqual(error.code, NSFileReadNoSuchFileError) 61 | XCTAssertNotNil(error.localizedDescription) 62 | expectation.fulfill() 63 | }) { _ in 64 | XCTFail("Expected to fail") 65 | expectation.fulfill() 66 | } 67 | 68 | self.waitForExpectations(timeout: 1, handler: nil) 69 | } 70 | 71 | func testFetchImage_Failure_HNKDiskEntityInvalidDataError() { 72 | let data = Data() 73 | try? data.write(to: URL(fileURLWithPath: sut.path), options: [.atomic]) 74 | 75 | let expectation = self.expectation(description: self.name) 76 | 77 | sut.fetch(failure: { 78 | guard let error = $0 as NSError? else { 79 | XCTFail("expected non-nil error"); 80 | expectation.fulfill() 81 | return 82 | } 83 | XCTAssertEqual(error.domain, HanekeGlobals.Domain) 84 | XCTAssertEqual(error.code, HanekeGlobals.DiskFetcher.ErrorCode.invalidData.rawValue) 85 | XCTAssertNotNil(error.localizedDescription) 86 | expectation.fulfill() 87 | }) { _ in 88 | XCTFail("Expected to fail") 89 | expectation.fulfill() 90 | } 91 | 92 | self.waitForExpectations(timeout: 1, handler: nil) 93 | } 94 | 95 | func testCancelFetch() { 96 | let image = UIImage.imageWithColor(UIColor.green) 97 | let data = image.pngData()! 98 | try? data.write(to: URL(fileURLWithPath: directoryPath), options: [.atomic]) 99 | sut.fetch(failure: { error in 100 | guard let error = error as NSError? else { 101 | XCTFail("expected non-nil error"); 102 | return 103 | } 104 | XCTFail("Unexpected failure with error \(error)") 105 | }) { _ in 106 | XCTFail("Unexpected success") 107 | } 108 | 109 | sut.cancelFetch() 110 | 111 | self.waitFor(0.1) 112 | } 113 | 114 | func testCancelFetch_NoFetch() { 115 | sut.cancelFetch() 116 | } 117 | 118 | // MARK: Cache extension 119 | 120 | func testCacheFetch_Success() { 121 | let data = Data.dataWithLength(1) 122 | let path = self.writeData(data) 123 | let expectation = self.expectation(description: self.name) 124 | let cache = Cache<Data>(name: self.name) 125 | 126 | _ = cache.fetch(path: path, failure: {_ in 127 | XCTFail("expected success") 128 | expectation.fulfill() 129 | }) { 130 | XCTAssertEqual($0, data) 131 | expectation.fulfill() 132 | } 133 | 134 | self.waitForExpectations(timeout: 1, handler: nil) 135 | 136 | cache.removeAll() 137 | } 138 | 139 | func testCacheFetch_Failure() { 140 | let path = (self.directoryPath as NSString).appendingPathComponent(self.name) 141 | let expectation = self.expectation(description: self.name) 142 | let cache = Cache<Data>(name: self.name) 143 | 144 | _ = cache.fetch(path: path, failure: {_ in 145 | expectation.fulfill() 146 | }) { _ in 147 | XCTFail("expected success") 148 | expectation.fulfill() 149 | } 150 | 151 | self.waitForExpectations(timeout: 1, handler: nil) 152 | 153 | cache.removeAll() 154 | } 155 | 156 | func testCacheFetch_WithFormat() { 157 | let data = Data.dataWithLength(1) 158 | let path = self.writeData(data) 159 | let expectation = self.expectation(description: self.name) 160 | let cache = Cache<Data>(name: self.name) 161 | let format = Format<Data>(name: self.name) 162 | cache.addFormat(format) 163 | 164 | _ = cache.fetch(path: path, formatName: format.name, failure: {_ in 165 | XCTFail("expected success") 166 | expectation.fulfill() 167 | }) { 168 | XCTAssertEqual($0, data) 169 | expectation.fulfill() 170 | } 171 | 172 | self.waitForExpectations(timeout: 1, handler: nil) 173 | 174 | cache.removeAll() 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /HanekeTests/DiskTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskTestCase.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/26/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class DiskTestCase : XCTestCase { 12 | 13 | lazy var directoryPath: String = { 14 | let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] 15 | let directoryPath = (documentsPath as NSString).appendingPathComponent(self.name) 16 | return directoryPath 17 | }() 18 | 19 | override func setUp() { 20 | super.setUp() 21 | try! FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil) 22 | } 23 | 24 | override func tearDown() { 25 | try! FileManager.default.removeItem(atPath: directoryPath) 26 | super.tearDown() 27 | } 28 | 29 | var dataIndex = 0 30 | 31 | func writeDataWithLength(_ length : Int) -> String { 32 | let data = Data.dataWithLength(length) 33 | return self.writeData(data) 34 | } 35 | 36 | func writeData(_ data : Data) -> String { 37 | let path = self.uniquePath() 38 | try? data.write(to: URL(fileURLWithPath: path), options: [.atomic]) 39 | return path 40 | } 41 | 42 | func uniquePath() -> String { 43 | let path = (self.directoryPath as NSString).appendingPathComponent("\(dataIndex)") 44 | dataIndex += 1 45 | return path 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /HanekeTests/FetchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/28/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class FetchTests : XCTestCase { 14 | 15 | var sut : Fetch<String>! 16 | 17 | override func setUp() { 18 | super.setUp() 19 | sut = Fetch<String>() 20 | } 21 | 22 | func testHasSucceded_True() { 23 | sut.succeed(self.name) 24 | 25 | XCTAssertTrue(sut.hasSucceeded) 26 | } 27 | 28 | func testHasSucceded_False() { 29 | XCTAssertFalse(sut.hasSucceeded) 30 | } 31 | 32 | func testHasSucceded_AfterFail_False() { 33 | sut.fail() 34 | 35 | XCTAssertFalse(sut.hasSucceeded) 36 | } 37 | 38 | func testHasFailed_True() { 39 | sut.fail() 40 | 41 | XCTAssertTrue(sut.hasFailed) 42 | } 43 | 44 | func testHasFailed_False() { 45 | XCTAssertFalse(sut.hasFailed) 46 | } 47 | 48 | func testHasSucceded_AfterSucceed_False() { 49 | sut.succeed(self.name) 50 | 51 | XCTAssertFalse(sut.hasFailed) 52 | } 53 | 54 | func testSucceed() { 55 | sut.succeed(self.name) 56 | } 57 | 58 | func testSucceed_AfterOnSuccess() { 59 | let value = self.name 60 | let expectation = self.expectation(description: value) 61 | sut.onSuccess { 62 | XCTAssertEqual($0, value) 63 | expectation.fulfill() 64 | } 65 | 66 | sut.succeed(value) 67 | 68 | self.waitForExpectations(timeout: 0, handler: nil) 69 | } 70 | 71 | func testFail() { 72 | sut.fail() 73 | } 74 | 75 | func testFail_AfterOnFailure() { 76 | let error = NSError(domain: self.name, code: 10, userInfo: nil) 77 | let expectation = self.expectation(description: self.name) 78 | sut.onFailure { 79 | XCTAssertEqual($0!.localizedDescription, error.localizedDescription) 80 | expectation.fulfill() 81 | } 82 | 83 | sut.fail(error) 84 | 85 | self.waitForExpectations(timeout: 0, handler: nil) 86 | } 87 | 88 | func testOnSuccess() { 89 | sut.onSuccess { _ in 90 | XCTFail("unexpected success") 91 | } 92 | } 93 | 94 | func testOnSuccess_AfterSucceed() { 95 | let value = self.name 96 | sut.succeed(value) 97 | let expectation = self.expectation(description: value) 98 | 99 | sut.onSuccess { 100 | XCTAssertEqual($0, value) 101 | expectation.fulfill() 102 | } 103 | 104 | self.waitForExpectations(timeout: 0, handler: nil) 105 | } 106 | 107 | func testOnFailure() { 108 | sut.onFailure { _ in 109 | XCTFail("unexpected failure") 110 | } 111 | } 112 | 113 | func testOnFailure_AfterFail() { 114 | let error = NSError(domain: self.name, code: 10, userInfo: nil) 115 | sut.fail(error) 116 | let expectation = self.expectation(description: self.name) 117 | 118 | sut.onFailure { 119 | XCTAssertEqual($0!.localizedDescription, error.localizedDescription) 120 | expectation.fulfill() 121 | } 122 | 123 | self.waitForExpectations(timeout: 0, handler: nil) 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /HanekeTests/FetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetcherTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class FetcherTests: XCTestCase { 14 | 15 | func testSimpleFetcherInit() { 16 | let key = self.name 17 | let image = UIImage.imageWithColor(UIColor.green) 18 | 19 | let fetcher = SimpleFetcher<UIImage>(key: key, value: image) 20 | 21 | XCTAssertEqual(fetcher.key, key) 22 | XCTAssertEqual(fetcher.getValue(), image) 23 | } 24 | 25 | func testSimpleFetcherFetch() { 26 | let key = self.name 27 | let image = UIImage.imageWithColor(UIColor.green) 28 | let fetcher = SimpleFetcher<UIImage>(key: key, value: image) 29 | let expectation = self.expectation(description: key) 30 | 31 | fetcher.fetch(failure: { _ in 32 | XCTFail("expected success") 33 | }) { 34 | XCTAssertEqual($0, image) 35 | expectation.fulfill() 36 | } 37 | 38 | self.waitForExpectations(timeout: 0, handler: nil) 39 | } 40 | 41 | func testCacheFetch() { 42 | let data = Data.dataWithLength(1) 43 | let expectation = self.expectation(description: self.name) 44 | let cache = Cache<Data>(name: self.name) 45 | 46 | _ = cache.fetch(key: self.name, value: data) { 47 | XCTAssertEqual($0, data) 48 | expectation.fulfill() 49 | } 50 | 51 | self.waitForExpectations(timeout: 1, handler: nil) 52 | 53 | cache.removeAll() 54 | } 55 | 56 | func testCacheFetch_WithFormat() { 57 | let data = Data.dataWithLength(1) 58 | let expectation = self.expectation(description: self.name) 59 | let cache = Cache<Data>(name: self.name) 60 | let format = Format<Data>(name: self.name) 61 | cache.addFormat(format) 62 | 63 | _ = cache.fetch(key: self.name, value: data, formatName: format.name) { 64 | XCTAssertEqual($0, data) 65 | expectation.fulfill() 66 | } 67 | 68 | self.waitForExpectations(timeout: 1, handler: nil) 69 | 70 | cache.removeAll() 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /HanekeTests/FormatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/27/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | @testable import Haneke 12 | 13 | class FormatTests: XCTestCase { 14 | 15 | func testDefaultInit() { 16 | let name = self.name 17 | let sut = Format<UIImage>(name: name) 18 | 19 | XCTAssertEqual(sut.name, name) 20 | XCTAssertEqual(sut.diskCapacity, UINT64_MAX) 21 | XCTAssertTrue(sut.transform == nil) 22 | } 23 | 24 | func testIsIdentity_WithoutTransform_ExpectTrue() { 25 | let sut = Format<UIImage>(name: self.name) 26 | 27 | XCTAssertTrue(sut.isIdentity) 28 | } 29 | 30 | func testIsIdentity_WithTransform_ExpectFalse() { 31 | let sut = Format<UIImage>(name: self.name, transform: { return $0 }) 32 | 33 | XCTAssertFalse(sut.isIdentity) 34 | } 35 | 36 | func testResizeImageScaleNone() { 37 | 38 | let originalImage = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 39 | let sut = ImageResizer(size: CGSize(width: 30, height: 5), scaleMode: .None) 40 | let resizedImage = sut.resizeImage(originalImage) 41 | 42 | XCTAssertEqual(originalImage.size.width, resizedImage.size.width) 43 | XCTAssertEqual(originalImage.size.height, resizedImage.size.height) 44 | XCTAssertNotEqual(Float(resizedImage.size.width), Float(30)) 45 | XCTAssertNotEqual(Float(resizedImage.size.height), Float(5)) 46 | } 47 | 48 | func testResizeImageScaleFill() { 49 | 50 | let originalImage = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 51 | let sut = ImageResizer(size: CGSize(width: 30, height: 5), scaleMode : .Fill) 52 | let resizedImage = sut.resizeImage(originalImage) 53 | 54 | XCTAssertNotEqual(originalImage.size.width, resizedImage.size.width) 55 | XCTAssertNotEqual(originalImage.size.height, resizedImage.size.height) 56 | XCTAssertEqual(Float(resizedImage.size.width), Float(30)) 57 | XCTAssertEqual(Float(resizedImage.size.height), Float(5)) 58 | } 59 | 60 | func testResizeImageScaleAspectFill() { 61 | 62 | let originalImage = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 63 | let sut = ImageResizer(size: CGSize(width: 30, height: 5), scaleMode: .AspectFill) 64 | let resizedImage = sut.resizeImage(originalImage) 65 | 66 | XCTAssertNotEqual(originalImage.size.width, resizedImage.size.width) 67 | XCTAssertNotEqual(originalImage.size.height, resizedImage.size.height) 68 | XCTAssertEqual(Float(resizedImage.size.width), Float(30)) 69 | XCTAssertEqual(Float(resizedImage.size.height), Float(30)) 70 | } 71 | 72 | func testResizeImageScaleAspectFit() { 73 | 74 | let originalImage = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 75 | let sut = ImageResizer(size: CGSize(width: 30, height: 5), scaleMode: .AspectFit) 76 | let resizedImage = sut.resizeImage(originalImage) 77 | 78 | XCTAssertNotEqual(originalImage.size.width, resizedImage.size.width) 79 | XCTAssertNotEqual(originalImage.size.height, resizedImage.size.height) 80 | XCTAssertEqual(Float(resizedImage.size.width), Float(5)) 81 | XCTAssertEqual(Float(resizedImage.size.height), Float(5)) 82 | } 83 | 84 | func testResizeImageScaleAspectFillWithoutUpscaling() { 85 | 86 | let originalImage = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 87 | let sut = ImageResizer(size: CGSize(width: 30, height: 5), scaleMode: .AspectFill, allowUpscaling: false) 88 | let resizedImage = sut.resizeImage(originalImage) 89 | 90 | XCTAssertEqual(originalImage.size.width, resizedImage.size.width) 91 | XCTAssertEqual(originalImage.size.height, resizedImage.size.height) 92 | XCTAssertEqual(Float(resizedImage.size.width), Float(1)) 93 | XCTAssertEqual(Float(resizedImage.size.height), Float(1)) 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /HanekeTests/HanekeTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import <OHHTTPStubs/OHHTTPStubs.h> 6 | -------------------------------------------------------------------------------- /HanekeTests/HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/9/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Haneke 11 | 12 | class HanekeTests: XCTestCase { 13 | 14 | func testErrorWithCode() { 15 | let code = 200 16 | let description = self.name 17 | let error = errorWithCode(code, description: description) 18 | 19 | XCTAssertEqual(error._domain, HanekeGlobals.Domain) 20 | XCTAssertEqual(error._code, code) 21 | XCTAssertEqual(error.localizedDescription, description) 22 | } 23 | 24 | func testSharedImageCache() { 25 | XCTAssertNoThrow(Shared.imageCache) 26 | } 27 | 28 | func testSharedDataCache() { 29 | XCTAssertNoThrow(_ = Shared.dataCache) 30 | } 31 | 32 | func testSharedStringCache() { 33 | XCTAssertNoThrow(_ = Shared.stringCache) 34 | } 35 | 36 | func testSharedJSONCache() { 37 | XCTAssertNoThrow(_ = Shared.JSONCache) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /HanekeTests/Info.plist: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>CFBundleDevelopmentRegion</key> 6 | <string>en</string> 7 | <key>CFBundleExecutable</key> 8 | <string>${EXECUTABLE_NAME}</string> 9 | <key>CFBundleIdentifier</key> 10 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> 11 | <key>CFBundleInfoDictionaryVersion</key> 12 | <string>6.0</string> 13 | <key>CFBundleName</key> 14 | <string>${PRODUCT_NAME}</string> 15 | <key>CFBundlePackageType</key> 16 | <string>BNDL</string> 17 | <key>CFBundleShortVersionString</key> 18 | <string>1.0</string> 19 | <key>CFBundleSignature</key> 20 | <string>????</string> 21 | <key>CFBundleVersion</key> 22 | <string>1</string> 23 | </dict> 24 | </plist> 25 | -------------------------------------------------------------------------------- /HanekeTests/NSData+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSData.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/23/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Data { 12 | 13 | static func dataWithLength(_ length : Int) -> Data { 14 | let buffer: [UInt8] = [UInt8](repeating: 0, count: length) 15 | // return Data(bytes: UnsafePointer<UInt8>(&buffer), count: length) 16 | let pointer = UnsafeRawPointer(buffer) 17 | 18 | return NSData(bytes: pointer, length: length) as Data 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /HanekeTests/NSFileManager+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/26/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Haneke 11 | 12 | class NSFileManager_HanekeTests: DiskTestCase { 13 | 14 | func testEnumerateContentsOfDirectoryAtPathEmpty() { 15 | let sut = FileManager.default 16 | 17 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.nameKey.rawValue, ascending: true, usingBlock: { (URL : Foundation.URL, index : Int, _) -> Void in 18 | XCTFail() 19 | }) 20 | } 21 | 22 | func testEnumerateContentsOfDirectoryAtPathStop() { 23 | let sut = FileManager.default 24 | _ = [self.writeDataWithLength(1), self.writeDataWithLength(2)] 25 | var count = 0 26 | 27 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.nameKey.rawValue, ascending: true) { (_ : URL, index : Int, stop : inout Bool) -> Void in 28 | count += 1 29 | stop = true 30 | } 31 | 32 | XCTAssertEqual(count, 1) 33 | } 34 | 35 | func testEnumerateContentsOfDirectoryAtPathNameAscending() { 36 | let sut = FileManager.default 37 | 38 | let paths = [self.writeDataWithLength(1), self.writeDataWithLength(2)].sorted(by: <) 39 | var resultPaths : [String] = [] 40 | var indexes : [Int] = [] 41 | 42 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.nameKey.rawValue, ascending: true) { (URL : Foundation.URL, index : Int, _) -> Void in 43 | resultPaths.append(URL.path) 44 | indexes.append(index) 45 | } 46 | 47 | XCTAssertEqual(resultPaths.count, 2) 48 | XCTAssertEqual(resultPaths, paths) 49 | XCTAssertEqual(indexes[0], 0) 50 | XCTAssertEqual(indexes[1], 1) 51 | } 52 | 53 | func testEnumerateContentsOfDirectoryAtPathNameDescending() { 54 | let sut = FileManager.default 55 | 56 | let paths = [self.writeDataWithLength(1), self.writeDataWithLength(2)].sorted(by: >) 57 | var resultPaths : [String] = [] 58 | var indexes : [Int] = [] 59 | 60 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.nameKey.rawValue, ascending: false) { (URL : Foundation.URL, index : Int, _) -> Void in 61 | resultPaths.append(URL.path) 62 | indexes.append(index) 63 | } 64 | 65 | XCTAssertEqual(resultPaths.count, 2) 66 | XCTAssertEqual(resultPaths, paths) 67 | XCTAssertEqual(indexes[0], 0) 68 | XCTAssertEqual(indexes[1], 1) 69 | } 70 | 71 | func testEnumerateContentsOfDirectoryAtPathFileSizeAscending() { 72 | let sut = FileManager.default 73 | 74 | let paths = [self.writeDataWithLength(1), self.writeDataWithLength(2)] 75 | var resultPaths : [String] = [] 76 | 77 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.fileSizeKey.rawValue, ascending: true) { (URL : Foundation.URL, index : Int, _) -> Void in 78 | resultPaths.append(URL.path) 79 | } 80 | 81 | XCTAssertEqual(resultPaths.count, 2) 82 | XCTAssertEqual(resultPaths, paths) 83 | } 84 | 85 | func testEnumerateContentsOfDirectoryAtPathFileSizeDescending() { 86 | let sut = FileManager.default 87 | 88 | let paths : [String] = [self.writeDataWithLength(1), self.writeDataWithLength(2)].reversed() 89 | var resultPaths : [String] = [] 90 | 91 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.fileSizeKey.rawValue, ascending: false) { (URL : Foundation.URL, index : Int, _) -> Void in 92 | resultPaths.append(URL.path) 93 | } 94 | 95 | XCTAssertEqual(resultPaths.count, 2) 96 | XCTAssertEqual(resultPaths, paths) 97 | } 98 | 99 | func testEnumerateContentsOfDirectoryAtPathModificationDateAscending() { 100 | let sut = FileManager.default 101 | 102 | let paths = [self.writeDataWithLength(1), self.writeDataWithLength(2)] 103 | try! sut.setAttributes([FileAttributeKey.modificationDate : Date.distantPast], ofItemAtPath: paths[0]) 104 | 105 | var resultPaths : [String] = [] 106 | 107 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: true) { (URL : Foundation.URL, index : Int, _) -> Void in 108 | resultPaths.append(URL.path) 109 | } 110 | 111 | XCTAssertEqual(resultPaths.count, 2) 112 | XCTAssertEqual(resultPaths, paths) 113 | } 114 | 115 | func testEnumerateContentsOfDirectoryAtPathModificationDateDescending() { 116 | let sut = FileManager.default 117 | 118 | let paths = [self.writeDataWithLength(1), self.writeDataWithLength(2)] 119 | try! sut.setAttributes([FileAttributeKey.modificationDate : Date.distantPast], ofItemAtPath: paths[1]) 120 | var resultPaths : [String] = [] 121 | 122 | sut.enumerateContentsOfDirectory(atPath: self.directoryPath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: false) { (URL : Foundation.URL, index : Int, _) -> Void in 123 | resultPaths.append(URL.path) 124 | } 125 | 126 | XCTAssertEqual(resultPaths.count, 2) 127 | XCTAssertEqual(resultPaths, paths) 128 | } 129 | 130 | } 131 | 132 | -------------------------------------------------------------------------------- /HanekeTests/NSHTTPURLResponse+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSHTTPURLResponse+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 1/2/16. 6 | // Copyright © 2016 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Haneke 11 | 12 | func responseWithStatusCode(_ statusCode : Int) -> HTTPURLResponse { 13 | return HTTPURLResponse(url: URL(string: "http://haneke.io")!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: nil)! 14 | } 15 | 16 | class NSHTTPURLResponse_HanekeTests: XCTestCase { 17 | 18 | func testIsValidStatusCode() { 19 | XCTAssertTrue(responseWithStatusCode(200).hnk_isValidStatusCode()) 20 | XCTAssertTrue(responseWithStatusCode(201).hnk_isValidStatusCode()) 21 | XCTAssertFalse(responseWithStatusCode(404).hnk_isValidStatusCode()) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /HanekeTests/NSURLResponse+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLResponse+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/15/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Haneke 11 | 12 | class NSURLResponse_HanekeTests: XCTestCase { 13 | 14 | let httpURL = URL(string: "http://haneke.io")! 15 | let fileURL = URL(string: "file:///image.png")! 16 | 17 | func testValidateLengthOfData_NSHTTPURLResponse_Unknown() { 18 | let response = HTTPURLResponse(url: httpURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)! 19 | let data = Data.dataWithLength(132) 20 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 21 | } 22 | 23 | func testValidateLengthOfData_NSHTTPURLResponse_Expected() { 24 | let length = 73 25 | let response = HTTPURLResponse(url: httpURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Length": String(length)])! 26 | let data = Data.dataWithLength(length) 27 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 28 | } 29 | 30 | func testValidateLengthOfData_NSHTTPURLResponse_LessThanExpected() { 31 | let length = 73 32 | let response = HTTPURLResponse(url: httpURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Length": String(length)])! 33 | let data = Data.dataWithLength(length - 10) 34 | XCTAssertFalse(response.hnk_validateLength(ofData: data)) 35 | } 36 | 37 | func testValidateLengthOfData_NSHTTPURLResponse_MoreThanExpected() { 38 | let length = 73 39 | let response = HTTPURLResponse(url: httpURL, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: ["Content-Length": String(length)])! 40 | let data = Data.dataWithLength(length + 10) 41 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 42 | } 43 | 44 | func testValidateLengthOfData_NSURLResponse_Unknown() { 45 | let response = URLResponse(url: fileURL, mimeType: "image/png", expectedContentLength: -1, textEncodingName: nil) 46 | let data = Data.dataWithLength(73) 47 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 48 | } 49 | 50 | func testValidateLengthOfData_NSURLResponse_Expected() { 51 | let length = 73 52 | let response = URLResponse(url: fileURL, mimeType: "image/png", expectedContentLength: length, textEncodingName: nil) 53 | let data = Data.dataWithLength(length) 54 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 55 | } 56 | 57 | func testValidateLengthOfData_NSURLResponse_LessThanExpected() { 58 | let length = 73 59 | let response = URLResponse(url: fileURL, mimeType: "image/png", expectedContentLength: length, textEncodingName: nil) 60 | let data = Data.dataWithLength(length - 10) 61 | XCTAssertFalse(response.hnk_validateLength(ofData: data)) 62 | } 63 | 64 | func testValidateLengthOfData_NSURLResponse_MoreThanExpected() { 65 | let length = 73 66 | let response = URLResponse(url: fileURL, mimeType: "image/png", expectedContentLength: length, textEncodingName: nil) 67 | let data = Data.dataWithLength(length + 10) 68 | XCTAssertTrue(response.hnk_validateLength(ofData: data)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /HanekeTests/NetworkFetcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkFetcherTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/15/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | import OHHTTPStubs 12 | @testable import Haneke 13 | 14 | class NetworkFetcherTests: XCTestCase { 15 | 16 | let URL = Foundation.URL(string: "http://haneke.io/image.jpg")! 17 | var sut : NetworkFetcher<UIImage>! 18 | 19 | override func setUp() { 20 | super.setUp() 21 | sut = NetworkFetcher(URL: URL) 22 | } 23 | 24 | override func tearDown() { 25 | OHHTTPStubs.removeAllStubs() 26 | super.tearDown() 27 | } 28 | 29 | func testInit() { 30 | XCTAssertEqual(sut.URL, URL) 31 | } 32 | 33 | func testKey() { 34 | XCTAssertEqual(sut.key, URL.absoluteString) 35 | } 36 | 37 | func testFetchImage_Success() { 38 | let image = UIImage.imageWithColor(UIColor.green) 39 | OHHTTPStubs.stubRequests(passingTest: { _ in 40 | return true 41 | }, withStubResponse: { _ in 42 | let data = image.pngData() 43 | return OHHTTPStubsResponse(data: data!, statusCode: 200, headers:nil) 44 | }) 45 | let expectation = self.expectation(description: self.name) 46 | 47 | sut.fetch(failure: { _ in 48 | XCTFail("expected success") 49 | expectation.fulfill() 50 | }) { 51 | let result = $0 as UIImage 52 | XCTAssertTrue(result.isEqualPixelByPixel(image)) 53 | expectation.fulfill() 54 | } 55 | 56 | self.waitForExpectations(timeout: 1, handler: nil) 57 | } 58 | 59 | func testFetchImage_Success_StatusCode200() { 60 | self.testFetchImageSuccessWithStatusCode(200) 61 | } 62 | 63 | func testFetchImage_Success_StatusCode201() { 64 | self.testFetchImageSuccessWithStatusCode(201) 65 | } 66 | 67 | func testFetchImage_Failure_InvalidStatusCode_401() { 68 | self.testFetchImageFailureWithInvalidStatusCode(401) 69 | } 70 | 71 | func testFetchImage_Failure_InvalidStatusCode_402() { 72 | self.testFetchImageFailureWithInvalidStatusCode(402) 73 | } 74 | 75 | func testFetchImage_Failure_InvalidStatusCode_403() { 76 | self.testFetchImageFailureWithInvalidStatusCode(403) 77 | } 78 | 79 | func testFetchImage_Failure_InvalidStatusCode_404() { 80 | self.testFetchImageFailureWithInvalidStatusCode(404) 81 | } 82 | 83 | func testFetchImage_Failure_InvalidData() { 84 | OHHTTPStubs.stubRequests(passingTest: { _ in 85 | return true 86 | }, withStubResponse: { _ in 87 | let data = Data() 88 | return OHHTTPStubsResponse(data: data, statusCode: 200, headers:nil) 89 | }) 90 | let expectation = self.expectation(description: self.name) 91 | 92 | sut.fetch(failure: { 93 | XCTAssertEqual($0!._domain, HanekeGlobals.Domain) 94 | XCTAssertEqual($0!._code, HanekeGlobals.NetworkFetcher.ErrorCode.invalidData.rawValue) 95 | XCTAssertNotNil($0!.localizedDescription) 96 | expectation.fulfill() 97 | }) { _ in 98 | XCTFail("expected failure") 99 | expectation.fulfill() 100 | } 101 | 102 | self.waitForExpectations(timeout: 100000, handler: nil) 103 | } 104 | 105 | func testFetchImage_Failure_MissingData() { 106 | OHHTTPStubs.stubRequests(passingTest: { _ in 107 | return true 108 | }, withStubResponse: { _ in 109 | let data = Data.dataWithLength(100) 110 | return OHHTTPStubsResponse(data: data, statusCode: 200, headers:["Content-Length":String(data.count * 2)]) 111 | }) 112 | let expectation = self.expectation(description: self.name) 113 | 114 | sut.fetch(failure: { 115 | XCTAssertEqual($0!._domain, HanekeGlobals.Domain) 116 | XCTAssertEqual($0!._code, HanekeGlobals.NetworkFetcher.ErrorCode.missingData.rawValue) 117 | XCTAssertNotNil($0!.localizedDescription) 118 | expectation.fulfill() 119 | }) { _ in 120 | XCTFail("expected failure") 121 | expectation.fulfill() 122 | } 123 | 124 | self.waitForExpectations(timeout: 1, handler: nil) 125 | } 126 | 127 | func testCancelFetch() { 128 | let image = UIImage.imageWithColor(UIColor.green) 129 | OHHTTPStubs.stubRequests(passingTest: { _ in 130 | return true 131 | }, withStubResponse: { _ in 132 | let data = image.pngData() 133 | return OHHTTPStubsResponse(data: data!, statusCode: 200, headers:nil) 134 | }) 135 | sut.fetch(failure: {_ in 136 | XCTFail("unexpected failure") 137 | }) { _ in 138 | XCTFail("unexpected success") 139 | } 140 | 141 | sut.cancelFetch() 142 | 143 | self.waitFor(0.1) 144 | } 145 | 146 | func testCancelFetch_NoFetch() { 147 | sut.cancelFetch() 148 | } 149 | 150 | func testSession() { 151 | XCTAssertEqual(sut.session, URLSession.shared) 152 | } 153 | 154 | // MARK: Private 155 | 156 | fileprivate func testFetchImageSuccessWithStatusCode(_ statusCode : Int32) { 157 | let image = UIImage.imageWithColor(UIColor.green) 158 | OHHTTPStubs.stubRequests(passingTest: { _ in 159 | return true 160 | }, withStubResponse: { _ in 161 | let data = image.pngData() 162 | return OHHTTPStubsResponse(data: data!, statusCode: statusCode, headers:nil) 163 | }) 164 | let expectation = self.expectation(description: self.name) 165 | sut.cancelFetch() 166 | 167 | sut.fetch(failure: { _ in 168 | XCTFail("expected success") 169 | expectation.fulfill() 170 | }) { 171 | let result = $0 as UIImage 172 | XCTAssertTrue(result.isEqualPixelByPixel(image)) 173 | expectation.fulfill() 174 | } 175 | 176 | self.waitForExpectations(timeout: 1, handler: nil) 177 | } 178 | 179 | fileprivate func testFetchImageFailureWithInvalidStatusCode(_ statusCode : Int32) { 180 | OHHTTPStubs.stubRequests(passingTest: { _ in 181 | return true 182 | }, withStubResponse: { _ in 183 | let data = Data.dataWithLength(100) 184 | return OHHTTPStubsResponse(data: data, statusCode: statusCode, headers:nil) 185 | }) 186 | let expectation = self.expectation(description: self.name) 187 | 188 | sut.fetch(failure: { 189 | XCTAssertEqual($0!._domain, HanekeGlobals.Domain) 190 | XCTAssertEqual($0!._code, HanekeGlobals.NetworkFetcher.ErrorCode.invalidStatusCode.rawValue) 191 | XCTAssertNotNil($0!.localizedDescription) 192 | expectation.fulfill() 193 | }) { _ in 194 | XCTFail("expected failure") 195 | expectation.fulfill() 196 | } 197 | 198 | self.waitForExpectations(timeout: 1, handler: nil) 199 | } 200 | 201 | // MARK: Cache extension 202 | 203 | func testCacheFetch_Success() { 204 | let data = Data.dataWithLength(1) 205 | OHHTTPStubs.stubRequests(passingTest: { _ in 206 | return true 207 | }, withStubResponse: { _ in 208 | return OHHTTPStubsResponse(data: data, statusCode: 200, headers:nil) 209 | }) 210 | let expectation = self.expectation(description: self.name) 211 | let cache = Cache<Data>(name: self.name) 212 | 213 | _ = cache.fetch(URL: URL, failure: {_ in 214 | XCTFail("expected success") 215 | expectation.fulfill() 216 | }) { 217 | XCTAssertEqual($0, data) 218 | expectation.fulfill() 219 | } 220 | 221 | self.waitForExpectations(timeout: 1, handler: nil) 222 | 223 | cache.removeAll() 224 | } 225 | 226 | func testCacheFetch_Failure() { 227 | let data = Data.dataWithLength(1) 228 | OHHTTPStubs.stubRequests(passingTest: { _ in 229 | return true 230 | }, withStubResponse: { _ in 231 | return OHHTTPStubsResponse(data: data, statusCode: 404, headers:nil) 232 | }) 233 | let expectation = self.expectation(description: self.name) 234 | let cache = Cache<Data>(name: self.name) 235 | 236 | _ = cache.fetch(URL: URL, failure: {_ in 237 | expectation.fulfill() 238 | }) { _ in 239 | XCTFail("expected success") 240 | expectation.fulfill() 241 | } 242 | 243 | self.waitForExpectations(timeout: 1, handler: nil) 244 | 245 | cache.removeAll() 246 | } 247 | 248 | func testCacheFetch_WithFormat() { 249 | let data = Data.dataWithLength(1) 250 | OHHTTPStubs.stubRequests(passingTest: { _ in 251 | return true 252 | }, withStubResponse: { _ in 253 | return OHHTTPStubsResponse(data: data, statusCode: 404, headers:nil) 254 | }) 255 | let expectation = self.expectation(description: self.name) 256 | let cache = Cache<Data>(name: self.name) 257 | let format = Format<Data>(name: self.name) 258 | cache.addFormat(format) 259 | 260 | _ = cache.fetch(URL: URL, formatName: format.name, failure: {_ in 261 | expectation.fulfill() 262 | }) { _ in 263 | XCTFail("expected success") 264 | expectation.fulfill() 265 | } 266 | 267 | self.waitForExpectations(timeout: 1, handler: nil) 268 | 269 | cache.removeAll() 270 | } 271 | 272 | } 273 | -------------------------------------------------------------------------------- /HanekeTests/String+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/30/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Haneke 11 | 12 | class String_HanekeTests: XCTestCase { 13 | 14 | func testEscapedFilename() { 15 | XCTAssertEqual("".escapedFilename(), "") 16 | XCTAssertEqual(":".escapedFilename(), "%3A") 17 | XCTAssertEqual("/".escapedFilename(), "%2F") 18 | XCTAssertEqual(" ".escapedFilename(), " ") 19 | XCTAssertEqual("\\".escapedFilename(), "\\") 20 | XCTAssertEqual("test".escapedFilename(), "test") 21 | XCTAssertEqual("http://haneke.io".escapedFilename(), "http%3A%2F%2Fhaneke.io") 22 | XCTAssertEqual("/path/to/file".escapedFilename(), "%2Fpath%2Fto%2Ffile") 23 | } 24 | 25 | func testMD5String() { 26 | XCTAssertEqual("".MD5String(), "d41d8cd98f00b204e9800998ecf8427e") 27 | XCTAssertEqual("Haneke".MD5String(), "aaf750bf2c41f921d0f5c1e9ba36f6f4") 28 | XCTAssertEqual("http://haneke.io".MD5String(), "e7bbf4e61be4fe99e3dd95f99b666aa0") 29 | XCTAssertEqual("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam pretium id nibh a pulvinar. Integer id ex in tellus egestas placerat. Praesent ultricies libero ligula, et convallis ligula imperdiet eu. Sed gravida, turpis sed vulputate feugiat, metus nisl scelerisque diam, ac aliquet metus nisi rutrum ipsum. Nulla vulputate pretium dolor, a pellentesque nulla. Nunc pellentesque tortor porttitor, sollicitudin leo in, sollicitudin ligula. Cras malesuada orci at neque interdum elementum. Integer sed sagittis diam. Mauris non elit sed augue consequat feugiat. Nullam volutpat tortor eget tempus pretium. Sed pharetra sem vitae diam hendrerit, sit amet dapibus arcu interdum. Fusce egestas quam libero, ut efficitur turpis placerat eu. Sed velit sapien, aliquam sit amet ultricies a, bibendum ac nibh. Maecenas imperdiet, quam quis tincidunt sollicitudin, nunc tellus ornare ipsum, nec rhoncus nunc nisi a lacus.".MD5String(), 30 | "36acb564fdf3c31c222c3069ba1d66d1") 31 | 32 | } 33 | 34 | func testMD5Filename() { 35 | XCTAssertEqual("".MD5Filename(), "".MD5String()) 36 | XCTAssertEqual("test".MD5Filename(), "test".MD5String()) 37 | XCTAssertEqual("test.png".MD5Filename(), ("test.png".MD5String() as NSString).appendingPathExtension("png")) 38 | } 39 | 40 | func testMD5Filename_QueryString() { 41 | let sut = "test.png?width=100&height=200" 42 | XCTAssertEqual(sut.MD5Filename(), (sut.MD5String() as NSString).appendingPathExtension("png")) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /HanekeTests/UIImage+HanekeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+HanekeTests.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 8/10/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | import ImageIO 12 | import MobileCoreServices 13 | @testable import Haneke 14 | 15 | enum ExifOrientation : UInt32 { 16 | case up = 1 17 | case down = 3 18 | case left = 8 19 | case right = 6 20 | case upMirrored = 2 21 | case downMirrored = 4 22 | case leftMirrored = 5 23 | case rightMirrored = 7 24 | } 25 | 26 | class UIImage_HanekeTests: XCTestCase { 27 | 28 | func testHasAlphaTrue() { 29 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 30 | XCTAssertTrue(image.hnk_hasAlpha()) 31 | } 32 | 33 | func testHasAlphaFalse() { 34 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), true) 35 | XCTAssertFalse(image.hnk_hasAlpha()) 36 | } 37 | 38 | func testDataPNG() { 39 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), false) 40 | let expectedData = image.pngData() 41 | 42 | let data = image.hnk_data() 43 | 44 | XCTAssertEqual(data!, expectedData) 45 | } 46 | 47 | func testDataJPEG() { 48 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 1, height: 1), true) 49 | let expectedData = image.jpegData(compressionQuality: 1) 50 | 51 | let data = image.hnk_data() 52 | 53 | XCTAssertEqual(data!, expectedData) 54 | } 55 | 56 | func testDataNil() { 57 | let image = UIImage() 58 | 59 | XCTAssertNil(image.hnk_data()) 60 | } 61 | 62 | func testDecompressedImage_UIGraphicsContext_Opaque() { 63 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 10)) 64 | 65 | let decompressedImage = image.hnk_decompressedImage() 66 | 67 | XCTAssertNotEqual(image, decompressedImage) 68 | XCTAssertTrue((decompressedImage?.isEqualPixelByPixel(image))!) 69 | } 70 | 71 | func testDecompressedImage_UIGraphicsContext_NotOpaque() { 72 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 10), false) 73 | 74 | let decompressedImage = image.hnk_decompressedImage() 75 | 76 | XCTAssertNotEqual(image, decompressedImage) 77 | XCTAssertTrue((decompressedImage?.isEqualPixelByPixel(image))!) 78 | } 79 | 80 | func testDecompressedImage_RGBA() { 81 | let color = UIColor(red:255, green:0, blue:0, alpha:0.5) 82 | self._testDecompressedImageUsingColor(color, alphaInfo: .premultipliedLast) 83 | } 84 | 85 | func testDecompressedImage_ARGB() { 86 | let color = UIColor(red:255, green:0, blue:0, alpha:0.5) 87 | self._testDecompressedImageUsingColor(color, alphaInfo: .premultipliedFirst) 88 | } 89 | 90 | func testDecompressedImage_RGBX() { 91 | self._testDecompressedImageUsingColor(alphaInfo: .noneSkipLast) 92 | } 93 | 94 | func testDecompressedImage_XRGB() { 95 | self._testDecompressedImageUsingColor(alphaInfo: .noneSkipFirst) 96 | } 97 | 98 | func testDecompressedImage_Gray_AlphaNone() { 99 | let color = UIColor.gray 100 | let colorSpaceRef = CGColorSpaceCreateDeviceGray() 101 | self._testDecompressedImageUsingColor(color, colorSpace: colorSpaceRef, alphaInfo: .none) 102 | } 103 | 104 | func testDecompressedImage_OrientationUp() { 105 | self._testDecompressedImageWithOrientation(.up) 106 | } 107 | 108 | func testDecompressedImage_OrientationDown() { 109 | self._testDecompressedImageWithOrientation(.down) 110 | } 111 | 112 | func testDecompressedImage_OrientationLeft() { 113 | self._testDecompressedImageWithOrientation(.left) 114 | } 115 | 116 | func testDecompressedImage_OrientationRight() { 117 | self._testDecompressedImageWithOrientation(.right) 118 | } 119 | 120 | func testDecompressedImage_OrientationUpMirrored() { 121 | self._testDecompressedImageWithOrientation(.upMirrored) 122 | } 123 | 124 | func testDecompressedImage_OrientationDownMirrored() { 125 | self._testDecompressedImageWithOrientation(.downMirrored) 126 | } 127 | 128 | func testDecompressedImage_OrientationLeftMirrored() { 129 | self._testDecompressedImageWithOrientation(.leftMirrored) 130 | } 131 | 132 | func testDecompressedImage_OrientationRightMirrored() { 133 | self._testDecompressedImageWithOrientation(.rightMirrored) 134 | } 135 | 136 | func testDataCompressionQuality() { 137 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 10)) 138 | let data = image.hnk_data() 139 | let notExpectedData = image.hnk_data(compressionQuality: 0.5) 140 | 141 | XCTAssertNotEqual(data, notExpectedData) 142 | } 143 | 144 | func testDataCompressionQuality_LessThan0() { 145 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 10)) 146 | let data = image.hnk_data(compressionQuality: -1.0) 147 | let expectedData = image.hnk_data(compressionQuality: 0.0) 148 | 149 | XCTAssertEqual(data, expectedData, "The min compression quality is 0.0") 150 | } 151 | 152 | func testDataCompressionQuality_MoreThan1() { 153 | let image = UIImage.imageWithColor(UIColor.red, CGSize(width: 10, height: 10)) 154 | let data = image.hnk_data(compressionQuality: 10.0) 155 | let expectedData = image.hnk_data(compressionQuality: 1.0) 156 | 157 | XCTAssertEqual(data, expectedData, "The min compression quality is 1.0") 158 | } 159 | 160 | // MARK: Helpers 161 | 162 | func _testDecompressedImageUsingColor(_ color : UIColor = UIColor.green, colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB(), alphaInfo :CGImageAlphaInfo, bitsPerComponent : size_t = 8) { 163 | let size = CGSize(width: 10, height: 20) // Using rectangle to check if image is rotated 164 | let bitmapInfo = CGBitmapInfo().rawValue | alphaInfo.rawValue 165 | let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo) 166 | 167 | context?.setFillColor(color.cgColor) 168 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) 169 | let imageRef = context?.makeImage()! 170 | 171 | let image = UIImage(cgImage: imageRef!, scale:UIScreen.main.scale, orientation:.up) 172 | let decompressedImage = image.hnk_decompressedImage() 173 | 174 | XCTAssertNotEqual(image, decompressedImage) 175 | XCTAssertTrue((decompressedImage?.isEqualPixelByPixel(image))!, self.name) 176 | } 177 | 178 | func _testDecompressedImageWithOrientation(_ orientation : ExifOrientation) { 179 | // Create a gradient image to truly test orientation 180 | let gradientImage = UIImage.imageGradientFromColor() 181 | 182 | // Use TIFF because PNG doesn't store EXIF orientation 183 | let exifProperties = NSDictionary(dictionary: [kCGImagePropertyOrientation: Int(orientation.rawValue)]) 184 | let data = NSMutableData() 185 | let imageDestinationRef = CGImageDestinationCreateWithData(data as CFMutableData, kUTTypeTIFF, 1, nil)! 186 | CGImageDestinationAddImage(imageDestinationRef, gradientImage.cgImage!, exifProperties as CFDictionary) 187 | CGImageDestinationFinalize(imageDestinationRef) 188 | 189 | let image = UIImage(data:data as Data, scale:UIScreen.main.scale)! 190 | 191 | let decompressedImage = image.hnk_decompressedImage() 192 | 193 | XCTAssertNotEqual(image, decompressedImage) 194 | XCTAssertTrue((decompressedImage?.isEqualPixelByPixel(image))!, self.name) 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /HanekeTests/UIImage+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Test.swift 3 | // Haneke 4 | // 5 | // Created by Oriol Blanc Gimeno on 01/08/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | func isEqualPixelByPixel(_ theOtherImage: UIImage) -> Bool { 14 | let imageData = self.normalizedData() 15 | let theOtherImageData = theOtherImage.normalizedData() 16 | return (imageData == theOtherImageData) 17 | } 18 | 19 | func normalizedData() -> Data { 20 | let pixelSize = CGSize(width : self.size.width * self.scale, height : self.size.height * self.scale) 21 | NSLog(NSCoder.string(for: pixelSize)) 22 | UIGraphicsBeginImageContext(pixelSize) 23 | self.draw(in: CGRect(x: 0, y: 0, width: pixelSize.width, height: pixelSize.height)) 24 | let drawnImage = UIGraphicsGetImageFromCurrentImageContext() 25 | UIGraphicsEndImageContext() 26 | let provider = drawnImage?.cgImage?.dataProvider 27 | let data = provider?.data 28 | return data! as Data 29 | } 30 | 31 | class func imageWithColor(_ color: UIColor, _ size: CGSize = CGSize(width: 1, height: 1), _ opaque: Bool = true) -> UIImage { 32 | UIGraphicsBeginImageContextWithOptions(size, opaque, 0) 33 | let context = UIGraphicsGetCurrentContext() 34 | context?.setFillColor(color.cgColor) 35 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) 36 | let image = UIGraphicsGetImageFromCurrentImageContext() 37 | UIGraphicsEndImageContext() 38 | return image! 39 | } 40 | 41 | class func imageGradientFromColor(_ fromColor : UIColor = UIColor.red, toColor : UIColor = UIColor.green, size : CGSize = CGSize(width: 10, height: 20)) -> UIImage { 42 | UIGraphicsBeginImageContextWithOptions(size, false /* opaque */, 0 /* scale */) 43 | let context = UIGraphicsGetCurrentContext() 44 | let colorspace = CGColorSpaceCreateDeviceRGB() 45 | let gradientNumberOfLocations : size_t = 2 46 | let gradientLocations : [CGFloat] = [ 0.0, 1.0 ] 47 | var r1 : CGFloat = 0, g1 : CGFloat = 0, b1 : CGFloat = 0, a1 : CGFloat = 0 48 | fromColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) 49 | var r2 : CGFloat = 0, g2 : CGFloat = 0 , b2 : CGFloat = 0, a2 : CGFloat = 0 50 | toColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) 51 | let gradientComponents = [r1, g1, b1, a1, r2, g2, b2, a2] 52 | let gradient = CGGradient (colorSpace: colorspace, colorComponents: gradientComponents, locations: gradientLocations, count: gradientNumberOfLocations) 53 | context?.drawLinearGradient(gradient!, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: size.height), options: CGGradientDrawingOptions()) 54 | let image = UIGraphicsGetImageFromCurrentImageContext() 55 | UIGraphicsEndImageContext() 56 | return image! 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /HanekeTests/XCTestCase+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestCase+Test.swift 3 | // Haneke 4 | // 5 | // Created by Hermes Pique on 9/15/14. 6 | // Copyright (c) 2014 Haneke. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | extension XCTestCase { 12 | 13 | func waitFor(_ interval : TimeInterval) { 14 | let date = Date(timeIntervalSinceNow: interval) 15 | RunLoop.current.run(mode: RunLoop.Mode.default, before: date) 16 | } 17 | 18 | func wait(_ timeout : TimeInterval, condition: () -> Bool) { 19 | let timeoutDate = Date(timeIntervalSinceNow: timeout) 20 | var success = false 21 | while !success && (NSDate().laterDate(timeoutDate) == timeoutDate) { 22 | success = condition() 23 | if !success { 24 | RunLoop.current.run(mode: RunLoop.Mode.default, before: timeoutDate) 25 | } 26 | } 27 | if !success { 28 | XCTFail("Wait timed out.") 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Haneke", 6 | platforms: [.iOS("8.0"), .tvOS("9.1")], 7 | products: [.library(name: "Haneke", targets: ["Haneke"])], 8 | targets: [.target(name: "Haneke", path: "Haneke")] 9 | ) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | [](https://github.com/Carthage/Carthage) 4 | [](https://github.com/apple/swift-package-manager) 5 | [](https://github.com/JamitLabs/Accio) 6 | [](http://cocoadocs.org/docsets/HanekeSwift) 7 | [](https://travis-ci.org/Haneke/HanekeSwift) 8 | [](https://gitter.im/Haneke/HanekeSwift?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | Haneke is a lightweight *generic* cache for iOS and tvOS written in Swift 4. It's designed to be super-simple to use. Here's how you would initalize a JSON cache and fetch objects from a url: 11 | 12 | ```swift 13 | let cache = Cache<JSON>(name: "github") 14 | let URL = NSURL(string: "https://api.github.com/users/haneke")! 15 | 16 | cache.fetch(URL: URL).onSuccess { JSON in 17 | print(JSON.dictionary?["bio"]) 18 | } 19 | ``` 20 | 21 | Haneke provides a memory and LRU disk cache for `UIImage`, `NSData`, `JSON`, `String` or any other type that can be read or written as data. 22 | 23 | Particularly, Haneke excels at working with images. It includes a zero-config image cache with automatic resizing. Everything is done in background, allowing for fast, responsive scrolling. Asking Haneke to load, resize, cache and display an *appropriately sized image* is as simple as: 24 | 25 | ```swift 26 | imageView.hnk_setImageFromURL(url) 27 | ``` 28 | 29 | _Really._ 30 | 31 | ## Features 32 | 33 | * Generic cache with out-of-the-box support for `UIImage`, `NSData`, `JSON` and `String` 34 | * First-level memory cache using `NSCache` 35 | * Second-level LRU disk cache using the file system 36 | * Asynchronous [fetching](#fetchers) of original values from network or disk 37 | * All disk access is performed in background 38 | * Thread-safe 39 | * Automatic cache eviction on memory warnings or disk capacity reached 40 | * Comprehensive unit tests 41 | * Extensible by defining [custom formats](#formats), supporting [additional types](#supporting-additional-types) or implementing [custom fetchers](#custom-fetchers) 42 | 43 | For images: 44 | 45 | * Zero-config `UIImageView` and `UIButton` extensions to use the cache, optimized for `UITableView` and `UICollectionView` cell reuse 46 | * Background image resizing and decompression 47 | 48 | ## Installation 49 | 50 | Using [CocoaPods](http://cocoapods.org/): 51 | 52 | ```ruby 53 | use_frameworks! 54 | pod 'HanekeSwift' 55 | ``` 56 | 57 | Using [Carthage](https://github.com/Carthage/Carthage): 58 | 59 | ``` 60 | github "Haneke/HanekeSwift" 61 | ``` 62 | 63 | Using [SwiftPM](https://github.com/apple/swift-package-manager) or [Accio](https://github.com/JamitLabs/Accio): 64 | 65 | 66 | ```swift 67 | .package(url: "https://github.com/Haneke/HanekeSwift.git", .upToNextMajor(from: "0.11.2")), 68 | ``` 69 | 70 | Then link `Haneke` in your App target like so: 71 | 72 | ```swift 73 | .target( 74 | name: "App", 75 | dependencies: [ 76 | "Haneke", 77 | ] 78 | ), 79 | ``` 80 | 81 | Manually: 82 | 83 | 1. Drag `Haneke.xcodeproj` to your project in the _Project Navigator_. 84 | 2. Select your project and then your app target. Open the _Build Phases_ panel. 85 | 3. Expand the _Target Dependencies_ group, and add `Haneke.framework`. 86 | 4. Click on the `+` button at the top left of the panel and select _New Copy Files Phase_. Set _Destination_ to _Frameworks_, and add `Haneke.framework`. 87 | 5. `import Haneke` whenever you want to use Haneke. 88 | 89 | ## Requirements 90 | 91 | - iOS 8.0+ or tvOS 9.1+ 92 | - Swift 4 93 | 94 | ## Using the cache 95 | 96 | Haneke provides shared caches for `UIImage`, `NSData`, `JSON` and `String`. You can also create your own caches. 97 | 98 | The cache is a key-value store. For example, here's how you would cache and then fetch some data. 99 | 100 | ```Swift 101 | let cache = Shared.dataCache 102 | 103 | cache.set(value: data, key: "funny-games.mp4") 104 | 105 | // Eventually... 106 | 107 | cache.fetch(key: "funny-games.mp4").onSuccess { data in 108 | // Do something with data 109 | } 110 | ``` 111 | 112 | In most cases the value will not be readily available and will have to be fetched from network or disk. Haneke offers convenience `fetch` functions for these cases. Let's go back to the first example, now using a shared cache: 113 | 114 | ```Swift 115 | let cache = Shared.JSONCache 116 | let URL = NSURL(string: "https://api.github.com/users/haneke")! 117 | 118 | cache.fetch(URL: URL).onSuccess { JSON in 119 | print(JSON.dictionary?["bio"]) 120 | } 121 | ``` 122 | 123 | The above call will first attempt to fetch the required JSON from (in order) memory, disk or `NSURLCache`. If not available, Haneke will fetch the JSON from the source, return it and then cache it. In this case, the URL itself is used as the key. 124 | 125 | Further customization can be achieved by using [formats](#formats), [supporting additional types](#supporting-additional-types) or implementing [custom fetchers](#custom-fetchers). 126 | 127 | ## Extra ♡ for images 128 | 129 | Need to cache and display images? Haneke provides convenience methods for `UIImageView` and `UIButton` with optimizations for `UITableView` and `UICollectionView` cell reuse. Images will be resized appropriately and cached in a shared cache. 130 | 131 | ```swift 132 | // Setting a remote image 133 | imageView.hnk_setImageFromURL(url) 134 | 135 | // Setting an image manually. Requires you to provide a key. 136 | imageView.hnk_setImage(image, key: key) 137 | ``` 138 | 139 | The above lines take care of: 140 | 141 | 1. If cached, retrieving an appropriately sized image (based on the `bounds` and `contentMode` of the `UIImageView`) from the memory or disk cache. Disk access is performed in background. 142 | 2. If not cached, loading the original image from web/memory and producing an appropriately sized image, both in background. Remote images will be retrieved from the shared `NSURLCache` if available. 143 | 3. Setting the image and animating the change if appropriate. 144 | 4. Or doing nothing if the `UIImageView` was reused during any of the above steps. 145 | 5. Caching the resulting image. 146 | 6. If needed, evicting the least recently used images in the cache. 147 | 148 | ## Formats 149 | 150 | Formats allow to specify the disk cache size and any transformations to the values before being cached. For example, the `UIImageView` extension uses a format that resizes images to fit or fill the image view as needed. 151 | 152 | You can also use custom formats. Say you want to limit the disk capacity for icons to 10MB and apply rounded corners to the images. This is how it could look like: 153 | 154 | ```swift 155 | let cache = Shared.imageCache 156 | 157 | let iconFormat = Format<UIImage>(name: "icons", diskCapacity: 10 * 1024 * 1024) { image in 158 | return imageByRoundingCornersOfImage(image) 159 | } 160 | cache.addFormat(iconFormat) 161 | 162 | let URL = NSURL(string: "http://haneke.io/icon.png")! 163 | cache.fetch(URL: URL, formatName: "icons").onSuccess { image in 164 | // image will be a nice rounded icon 165 | } 166 | ``` 167 | 168 | Because we told the cache to use the `"icons"` format Haneke will execute the format transformation in background and return the resulting value. 169 | 170 | Formats can also be used from the `UIKit` extensions: 171 | 172 | ```swift 173 | imageView.hnk_setImageFromURL(url, format: iconFormat) 174 | ``` 175 | 176 | ## Fetchers 177 | 178 | The `fetch` functions for urls and paths are actually convenience methods. Under the hood Haneke uses fetcher objects. To illustrate, here's another way of fetching from a url by explictly using a network fetcher: 179 | 180 | ```swift 181 | let URL = NSURL(string: "http://haneke.io/icon.png")! 182 | let fetcher = NetworkFetcher<UIImage>(URL: URL) 183 | cache.fetch(fetcher: fetcher).onSuccess { image in 184 | // Do something with image 185 | } 186 | ``` 187 | 188 | Fetching an original value from network or disk is an expensive operation. Fetchers act as a proxy for the value, and allow Haneke to perform the fetch operation only if absolutely necessary. 189 | 190 | In the above example the fetcher will be executed only if there is no value associated with `"http://haneke.io/icon.png"` in the memory or disk cache. If that happens, the fetcher will be responsible from fetching the original value, which will then be cached to avoid further network activity. 191 | 192 | Haneke provides two specialized fetchers: `NetworkFetcher<T>` and `DiskFetcher<T>`. You can also implement your own fetchers by subclassing `Fetcher<T>`. 193 | 194 | ### Custom fetchers 195 | 196 | Through custom fetchers you can fetch original values from other sources than network or disk (e.g., Core Data), or even change how Haneke acceses network or disk (e.g., use [Alamofire](https://github.com/Alamofire/Alamofire) for networking instead of `NSURLSession`). A custom fetcher must subclass `Fetcher<T>` and is responsible for: 197 | 198 | * Providing the key (e.g., `NSURL.absoluteString` in the case of `NetworkFetcher`) associated with the value to be fetched 199 | * Fetching the value in background and calling the success or failure closure accordingly, both in the main queue 200 | * Cancelling the fetch on demand, if possible 201 | 202 | Fetchers are generic, and the only restriction on their type is that it must implement `DataConvertible`. 203 | 204 | ## Supporting additional types 205 | 206 | Haneke can cache any type that can be read and saved as data. This is indicated to Haneke by implementing the protocols `DataConvertible` and `DataRepresentable`. 207 | 208 | ```Swift 209 | public protocol DataConvertible { 210 | typealias Result 211 | 212 | class func convertFromData(data:NSData) -> Result? 213 | 214 | } 215 | 216 | public protocol DataRepresentable { 217 | 218 | func asData() -> NSData! 219 | 220 | } 221 | ``` 222 | 223 | This is how one could add support for `NSDictionary`: 224 | 225 | ```Swift 226 | extension NSDictionary : DataConvertible, DataRepresentable { 227 | 228 | public typealias Result = NSDictionary 229 | 230 | public class func convertFromData(data:NSData) -> Result? { 231 | return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary 232 | } 233 | 234 | public func asData() -> NSData! { 235 | return NSKeyedArchiver.archivedDataWithRootObject(self) 236 | } 237 | 238 | } 239 | ``` 240 | 241 | Then creating a `NSDictionary` cache would be as simple as: 242 | 243 | ```swift 244 | let cache = Cache<NSDictionary>(name: "dictionaries") 245 | ``` 246 | 247 | ## Roadmap 248 | 249 | Haneke Swift is in initial development and its public API should not be considered stable. 250 | 251 | ## License 252 | 253 | Copyright 2014 Hermes Pique ([@hpique](https://twitter.com/hpique)) 254 | 2014 Joan Romano ([@joanromano](https://twitter.com/joanromano)) 255 | 2014 Luis Ascorbe ([@lascorbe](https://twitter.com/Lascorbe)) 256 | 2014 Oriol Blanc ([@oriolblanc](https://twitter.com/oriolblanc)) 257 | 258 | Licensed under the Apache License, Version 2.0 (the "License"); 259 | you may not use this file except in compliance with the License. 260 | You may obtain a copy of the License at 261 | 262 | http://www.apache.org/licenses/LICENSE-2.0 263 | 264 | Unless required by applicable law or agreed to in writing, software 265 | distributed under the License is distributed on an "AS IS" BASIS, 266 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 267 | See the License for the specific language governing permissions and 268 | limitations under the License. 269 | --------------------------------------------------------------------------------