├── include ├── Cashier.h ├── NOPersistentStore.h ├── NSString+MD5Addition.h └── module.modulemap ├── Cashier_icon.png ├── CashierTests ├── Nodes.png ├── CashierTests-Bridging-Header.h ├── NSBundle+Swizzle.h ├── NSMutableDictionary+Swizzle.h ├── NSBundle+Swizzle.m ├── NSMutableDictionary+Swizzle.m ├── Song.swift ├── Info.plist ├── SwiftTypesTests.swift └── CashierTests.m ├── codecov.yml ├── Cashier ├── Supporting Files │ ├── module.modulemap │ ├── Cashier-header.h │ └── Info.plist └── Classes │ ├── NSString+MD5Addition.h │ ├── NOPersistentStore.h │ ├── NSString+MD5Addition.m │ ├── NOPersistentStore.m │ ├── Cashier.h │ └── Cashier.m ├── Cashier.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── Cashier watchOS.xcscheme │ │ ├── Cashier.xcscheme │ │ ├── Cashier macOS.xcscheme │ │ └── Cashier tvOS.xcscheme └── project.pbxproj ├── CHANGELOG.md ├── LICENSE ├── .circleci └── config.yml ├── Package.swift ├── .gitignore ├── Cashier.podspec └── README.md /include/Cashier.h: -------------------------------------------------------------------------------- 1 | ../Cashier/Classes/Cashier.h -------------------------------------------------------------------------------- /include/NOPersistentStore.h: -------------------------------------------------------------------------------- 1 | ../Cashier/Classes/NOPersistentStore.h -------------------------------------------------------------------------------- /include/NSString+MD5Addition.h: -------------------------------------------------------------------------------- 1 | ../Cashier/Classes/NSString+MD5Addition.h -------------------------------------------------------------------------------- /Cashier_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cashier/master/Cashier_icon.png -------------------------------------------------------------------------------- /CashierTests/Nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/Cashier/master/CashierTests/Nodes.png -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | ignore: 5 | - "CashierTests/.*" 6 | -------------------------------------------------------------------------------- /include/module.modulemap: -------------------------------------------------------------------------------- 1 | module Cashier { 2 | umbrella header "Cashier.h" 3 | 4 | export * 5 | module * { export * } 6 | } -------------------------------------------------------------------------------- /Cashier/Supporting Files/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module Cashier { 2 | umbrella header "Cashier-header.h" 3 | 4 | export * 5 | module * { export * } 6 | } -------------------------------------------------------------------------------- /CashierTests/CashierTests-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 "Cashier.h" 6 | #import "NOPersistentStore.h" 7 | -------------------------------------------------------------------------------- /Cashier.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.1](https://github.com/nodes-ios/Cashier/releases/tag/1.0.1) 2 | Released on 2016-04-13. 3 | 4 | - Updated project structure 5 | - Added podspec 6 | 7 | ## [1.0.0](https://github.com/nodes-ios/Cashier/releases/tag/1.0.0) 8 | Released on 2016-03-19 9 | 10 | #### Added 11 | - Initial release of Cashier. -------------------------------------------------------------------------------- /Cashier.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cashier/Classes/NSString+MD5Addition.h: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueIdentifier.m 3 | // 4 | // 5 | // Created by Nils Munch on 3/12/12. 6 | // Copyright (c) 2012 Nodes. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString(MD5Addition) 12 | 13 | - (NSString *) stringFromMD5; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /CashierTests/NSBundle+Swizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+Swizzle.h 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 14/03/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSBundle (Swizzle) 12 | 13 | - (id)szl_objectForInfoDictionaryKey:(NSString*)key; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /CashierTests/NSMutableDictionary+Swizzle.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableDictionary+Swizzle.h 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 14/03/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSMutableDictionary (Swizzle) 12 | 13 | - (id)szl_objectForKey:(NSString*)key; 14 | @end 15 | -------------------------------------------------------------------------------- /Cashier/Classes/NOPersistentStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // NOPersistentStore.h 3 | // 4 | // 5 | // Created by Jakob Mygind on 03/09/14. 6 | // Copyright (c) 2014 Nodes. All rights reserved. 7 | // 8 | 9 | #import "Cashier.h" 10 | 11 | /** 12 | 13 | NOPersistentStore is a special type of Cashier. On the outside, it works exactly the 14 | same as a Cashier, but unlike the Cashier, it saves the cached objects in a folder 15 | that doesn't get cleared by the system when the device runs out of space. 16 | 17 | */ 18 | @interface NOPersistentStore : Cashier 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /CashierTests/NSBundle+Swizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+Swizzle.m 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 14/03/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import "NSBundle+Swizzle.h" 10 | 11 | @implementation NSBundle (Swizzle) 12 | 13 | 14 | 15 | - (id)szl_objectForInfoDictionaryKey:(NSString*)key { 16 | if ([key isEqualToString:@"CFBundleShortVersionString"]) { 17 | return @"0.0.2"; 18 | } else { 19 | return [self szl_objectForInfoDictionaryKey:key]; 20 | } 21 | } 22 | 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /CashierTests/NSMutableDictionary+Swizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSMutableDictionary+Swizzle.m 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 14/03/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import "NSMutableDictionary+Swizzle.h" 10 | 11 | @implementation NSMutableDictionary (Swizzle) 12 | 13 | - (id)szl_objectForKey:(id)key { 14 | if ([key isKindOfClass:[NSString class]] && [key isEqualToString:@"szl_cacheID2"]) { 15 | return nil; 16 | } else { 17 | return [self szl_objectForKey:key]; 18 | } 19 | } 20 | @end 21 | -------------------------------------------------------------------------------- /CashierTests/Song.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Song.swift 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 22/01/2017. 6 | // Copyright © 2017 Nodes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Song : Equatable { 12 | var id = 0 13 | var name = "" 14 | var artist = "" 15 | var album = "" 16 | var duration : TimeInterval = 0 17 | 18 | public static func ==(lhs: Song, rhs: Song) -> Bool { 19 | return lhs.id == rhs.id && lhs.name == rhs.name && lhs.artist == rhs.artist && lhs.album == rhs.album && lhs.duration == rhs.duration 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Cashier/Supporting Files/Cashier-header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Cashier.h 3 | // Cashier 4 | // 5 | // Created by Chris Combs on 09/02/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import "Cashier.h" 10 | #import "NOPersistentStore.h" 11 | //! Project version number for Cashier. 12 | FOUNDATION_EXPORT double CashierVersionNumber; 13 | 14 | //! Project version string for Cashier. 15 | FOUNDATION_EXPORT const unsigned char CashierVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /CashierTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Cashier/Classes/NSString+MD5Addition.m: -------------------------------------------------------------------------------- 1 | // 2 | // UniqueIdentifier.m 3 | // 4 | // 5 | // Created by Nils Munch on 3/12/12. 6 | // Copyright (c) 2012 Nodes. All rights reserved. 7 | // 8 | 9 | #import "NSString+MD5Addition.h" 10 | #import 11 | 12 | @implementation NSString(MD5Addition) 13 | 14 | - (NSString *) stringFromMD5{ 15 | 16 | if(self == nil || [self length] == 0) 17 | return nil; 18 | 19 | const char *value = [self UTF8String]; 20 | 21 | unsigned char outputBuffer[CC_MD5_DIGEST_LENGTH]; 22 | CC_MD5(value, (CC_LONG) strlen(value), outputBuffer); 23 | 24 | NSMutableString *outputString = [[NSMutableString alloc] initWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; 25 | for(NSInteger count = 0; count < CC_MD5_DIGEST_LENGTH; count++){ 26 | [outputString appendFormat:@"%02x",outputBuffer[count]]; 27 | } 28 | 29 | return outputString; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Cashier/Classes/NOPersistentStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // CICache.m 3 | // CityGuides 4 | // 5 | // Created by Jakob Mygind on 03/09/14. 6 | // Copyright (c) 2014 Nodes. All rights reserved. 7 | // 8 | 9 | #import "NOPersistentStore.h" 10 | 11 | @implementation NOPersistentStore 12 | 13 | + (NSString *)baseSaveDirectory 14 | { 15 | return [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"NOCache"]; 16 | } 17 | 18 | - (void)fileURLWillBeWritten:(NSURL *)url 19 | { 20 | NSError *error; 21 | [url setResourceValue:@YES forKey: NSURLIsExcludedFromBackupKey error:&error]; 22 | 23 | if ( error ) { 24 | NSLog(@"What?? %@", error); 25 | } 26 | } 27 | 28 | + (instancetype)defaultCache 29 | { 30 | return [self cacheWithId:@"INTERNAL_PERSISTENT_DEFAULT_CACHE"]; 31 | } 32 | 33 | - (BOOL)persistsCacheAcrossVersions 34 | { 35 | return YES; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Cashier/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nodes Agency - iOS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # iOS CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/ios-migrating-from-1-2/ for more details 4 | # 5 | 6 | ### Cashier config file ### 7 | version: 2 8 | jobs: 9 | build: 10 | 11 | # Specify the Xcode version to use 12 | macos: 13 | xcode: "9.4.1" 14 | 15 | steps: 16 | - checkout 17 | 18 | # Install CocoaPods 19 | # - run: 20 | # name: Install CocoaPods 21 | # command: pod install 22 | 23 | # Build the app and run tests 24 | - run: 25 | name: Build and run tests 26 | command: fastlane scan 27 | environment: 28 | SCAN_DEVICE: iPhone 6 29 | SCAN_SCHEME: Cashier 30 | 31 | # Collect XML test results data to show in the UI, 32 | # and save the same XML files under test-results folder 33 | # in the Artifacts tab 34 | - store_test_results: 35 | path: test_output/report.xml 36 | - store_artifacts: 37 | path: /tmp/test-results 38 | destination: scan-test-results 39 | - store_artifacts: 40 | path: ~/Library/Logs/scan 41 | destination: scan-logs -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Cashier", 8 | platforms: [ .iOS(.v10), .macOS(.v10_10), .watchOS(.v2), .tvOS(.v9) ], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "Cashier", 13 | targets: ["Cashier"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "Cashier", 24 | dependencies: [], 25 | path: ".", 26 | sources: [ 27 | "Cashier/Classes/NSString+MD5Addition.m", 28 | "Cashier/Classes/Cashier.m", 29 | "Cashier/Classes/NOPersistentStore.m" 30 | ], 31 | publicHeadersPath: "include" 32 | ) 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | .build/ 8 | DerivedData 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | 21 | ## Other 22 | *.xccheckout 23 | *.moved-aside 24 | *.xcuserstate 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 52 | 53 | fastlane/report.xml 54 | fastlane/screenshots 55 | -------------------------------------------------------------------------------- /Cashier.xcodeproj/xcshareddata/xcschemes/Cashier watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Cashier.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint Cashier.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "Cashier" 19 | s.version = "1.2.1" 20 | s.summary = "An easy to use 2-layered object cache for iOS." 21 | 22 | # This description is used to generate tags and improve search results. 23 | # * Think: What does it do? Why did you write it? What is the focus? 24 | # * Try to keep it short, snappy and to the point. 25 | # * Write the description between the DESC delimiters below. 26 | # * Finally, don't worry about the indent, CocoaPods strips it! 27 | s.description = <<-DESC 28 | Cashier is a caching framework that makes it easy to work with persistent data. 29 | DESC 30 | 31 | s.homepage = "https://github.com/nodes-ios/Cashier" 32 | 33 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 34 | # 35 | # Licensing your code is important. See http://choosealicense.com for more info. 36 | # CocoaPods will detect a license file if there is a named LICENSE* 37 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 38 | # 39 | 40 | s.license = "MIT" 41 | 42 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 43 | # 44 | # Specify the authors of the library, with email addresses. Email addresses 45 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 46 | # accepts just a name if you'd rather not provide an email address. 47 | # 48 | # Specify a social_media_url where others can refer to, for example a twitter 49 | # profile URL. 50 | # 51 | 52 | s.author = { "Nodes Agency - iOS" => "ios@nodes.dk" } 53 | s.social_media_url = "http://twitter.com/nodes_ios" 54 | 55 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 56 | # 57 | # If this Pod runs only on iOS or OS X, then specify the platform and 58 | # the deployment target. You can optionally include the target after the platform. 59 | # 60 | 61 | s.platforms = { :ios => "8.0", :osx => "10.10", :watchos => "2.0", :tvos => "9.0" } 62 | 63 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 64 | # 65 | # Specify the location from where the source should be retrieved. 66 | # Supports git, hg, bzr, svn and HTTP. 67 | # 68 | 69 | s.source = { :git => "https://github.com/nodes-ios/Cashier.git", :tag => s.version } 70 | 71 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 72 | # 73 | # CocoaPods is smart about how it includes source code. For source files 74 | # giving a folder will include any swift, h, m, mm, c & cpp files. 75 | # For header files it will include any header in the folder. 76 | # Not including the public_header_files will make all headers public. 77 | # 78 | 79 | s.source_files = "Cashier/Classes/*" 80 | 81 | end 82 | -------------------------------------------------------------------------------- /Cashier.xcodeproj/xcshareddata/xcschemes/Cashier.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Cashier.xcodeproj/xcshareddata/xcschemes/Cashier macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Cashier.xcodeproj/xcshareddata/xcschemes/Cashier tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /CashierTests/SwiftTypesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftTypesTests.swift 3 | // Cashier 4 | // 5 | // Created by Marius Constantinescu on 22/01/2017. 6 | // Copyright © 2017 Nodes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Cashier 11 | import UIKit 12 | 13 | class SwiftTypesTests: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | super.tearDown() 23 | } 24 | 25 | func testStoringSwiftTypesInDefaultCache() { 26 | let cache : Cashier! = Cashier.cache(withId: "bestCacheId") 27 | 28 | let double : Double = 2.0 29 | cache.setObject(double, forKey: "double") 30 | if let cachedDouble = cache.object(forKey: "double") as? Double { 31 | XCTAssert(cachedDouble == double) 32 | } else { 33 | XCTFail() 34 | } 35 | 36 | let int : Int = 2 37 | cache.setObject(int, forKey: "int") 38 | if let cachedInt = cache.object(forKey: "int") as? Int { 39 | XCTAssert(cachedInt == int) 40 | } else { 41 | XCTFail() 42 | } 43 | 44 | let float : Float = 2.0 45 | cache.setObject(float, forKey: "float") 46 | if let cachedFloat = cache.object(forKey: "float") as? Float { 47 | XCTAssert(cachedFloat == float) 48 | } else { 49 | XCTFail() 50 | } 51 | 52 | let bool : Bool = true 53 | cache.setObject(bool, forKey: "bool") 54 | if let cachedBool = cache.object(forKey: "bool") as? Bool { 55 | XCTAssert(cachedBool == bool) 56 | } else { 57 | XCTFail() 58 | } 59 | 60 | let string : String = "a string" 61 | cache.setObject(string, forKey: "string") 62 | if let cachedString = cache.object(forKey: "string") as? String { 63 | XCTAssert(cachedString == string) 64 | } else { 65 | XCTFail() 66 | } 67 | 68 | let doubleArray : [Double] = [2.0, 3.0] 69 | cache.setObject(doubleArray, forKey: "doubleArray") 70 | if let cachedDoubleArray = cache.object(forKey: "doubleArray") as? [Double] { 71 | XCTAssert(cachedDoubleArray == doubleArray) 72 | } else { 73 | XCTFail() 74 | } 75 | 76 | let intArray : [Int] = [2, 3] 77 | cache.setObject(intArray, forKey: "intArray") 78 | if let cachedIntArray = cache.object(forKey: "intArray") as? [Int] { 79 | XCTAssert(cachedIntArray == intArray) 80 | } else { 81 | XCTFail() 82 | } 83 | 84 | let floatArray : [Float] = [2.0, 3.0] 85 | cache.setObject(floatArray, forKey: "floatArray") 86 | if let cachedFloatArray = cache.object(forKey: "floatArray") as? [Float] { 87 | XCTAssert(cachedFloatArray == floatArray) 88 | } else { 89 | XCTFail() 90 | } 91 | 92 | let boolArray : [Bool] = [true, false] 93 | cache.setObject(boolArray, forKey: "boolArray") 94 | if let cachedBoolArray = cache.object(forKey: "boolArray") as? [Bool] { 95 | XCTAssert(cachedBoolArray == boolArray) 96 | } else { 97 | XCTFail() 98 | } 99 | 100 | let stringArray : [String] = ["a string", "another string"] 101 | cache.setObject(stringArray, forKey: "stringArray") 102 | if let cachedStringArray = cache.object(forKey: "stringArray") as? [String] { 103 | XCTAssert(cachedStringArray == stringArray) 104 | } else { 105 | XCTFail() 106 | } 107 | 108 | let song = Song(id: 14, name: "name", artist: "artist", album: "album", duration: 184) 109 | cache.setObject(song, forKey: "song") 110 | if let cachedSong = cache.object(forKey: "song") as? Song { 111 | XCTAssert(cachedSong == song) 112 | } else { 113 | XCTFail() 114 | } 115 | 116 | let image : UIImage! = UIImage(named: "Nodes", in: Bundle(for: type(of: self)), compatibleWith: nil) 117 | cache.setImage(image, forKey: "image") 118 | if let cachedImage = cache.image(forKey: "image") { 119 | XCTAssert(cachedImage == image) 120 | } else { 121 | XCTFail() 122 | } 123 | 124 | let data : Data! = string.data(using: .utf8) 125 | cache.setData(data, forKey: "data") 126 | if let cachedData = cache.data(forKey: "data") { 127 | XCTAssert(cachedData == data) 128 | } else { 129 | XCTFail() 130 | } 131 | } 132 | 133 | func testStoringSwiftTypesInPersistentStore() { 134 | let cache : NOPersistentStore! = NOPersistentStore.cache(withId: "bestPersistentStoreId") 135 | 136 | let double : Double = 2.0 137 | cache.setObject(double, forKey: "double") 138 | if let cachedDouble = cache.object(forKey: "double") as? Double { 139 | XCTAssert(cachedDouble == double) 140 | } else { 141 | XCTFail() 142 | } 143 | 144 | let int : Int = 2 145 | cache.setObject(int, forKey: "int") 146 | if let cachedInt = cache.object(forKey: "int") as? Int { 147 | XCTAssert(cachedInt == int) 148 | } else { 149 | XCTFail() 150 | } 151 | 152 | let float : Float = 2.0 153 | cache.setObject(float, forKey: "float") 154 | if let cachedFloat = cache.object(forKey: "float") as? Float { 155 | XCTAssert(cachedFloat == float) 156 | } else { 157 | XCTFail() 158 | } 159 | 160 | let bool : Bool = true 161 | cache.setObject(bool, forKey: "bool") 162 | if let cachedBool = cache.object(forKey: "bool") as? Bool { 163 | XCTAssert(cachedBool == bool) 164 | } else { 165 | XCTFail() 166 | } 167 | 168 | let string : String = "a string" 169 | cache.setObject(string, forKey: "string") 170 | if let cachedString = cache.object(forKey: "string") as? String { 171 | XCTAssert(cachedString == string) 172 | } else { 173 | XCTFail() 174 | } 175 | 176 | let doubleArray : [Double] = [2.0, 3.0] 177 | cache.setObject(doubleArray, forKey: "doubleArray") 178 | if let cachedDoubleArray = cache.object(forKey: "doubleArray") as? [Double] { 179 | XCTAssert(cachedDoubleArray == doubleArray) 180 | } else { 181 | XCTFail() 182 | } 183 | 184 | let intArray : [Int] = [2, 3] 185 | cache.setObject(intArray, forKey: "intArray") 186 | if let cachedIntArray = cache.object(forKey: "intArray") as? [Int] { 187 | XCTAssert(cachedIntArray == intArray) 188 | } else { 189 | XCTFail() 190 | } 191 | 192 | let floatArray : [Float] = [2.0, 3.0] 193 | cache.setObject(floatArray, forKey: "floatArray") 194 | if let cachedFloatArray = cache.object(forKey: "floatArray") as? [Float] { 195 | XCTAssert(cachedFloatArray == floatArray) 196 | } else { 197 | XCTFail() 198 | } 199 | 200 | let boolArray : [Bool] = [true, false] 201 | cache.setObject(boolArray, forKey: "boolArray") 202 | if let cachedBoolArray = cache.object(forKey: "boolArray") as? [Bool] { 203 | XCTAssert(cachedBoolArray == boolArray) 204 | } else { 205 | XCTFail() 206 | } 207 | 208 | let stringArray : [String] = ["a string", "another string"] 209 | cache.setObject(stringArray, forKey: "stringArray") 210 | if let cachedStringArray = cache.object(forKey: "stringArray") as? [String] { 211 | XCTAssert(cachedStringArray == stringArray) 212 | } else { 213 | XCTFail() 214 | } 215 | 216 | let song = Song(id: 14, name: "name", artist: "artist", album: "album", duration: 184) 217 | cache.setObject(song, forKey: "song") 218 | if let cachedSong = cache.object(forKey: "song") as? Song { 219 | XCTAssert(cachedSong == song) 220 | } else { 221 | XCTFail() 222 | } 223 | 224 | let image : UIImage! = UIImage(named: "Nodes", in: Bundle(for: type(of: self)), compatibleWith: nil) 225 | cache.setImage(image, forKey: "image") 226 | if let cachedImage = cache.image(forKey: "image") { 227 | XCTAssert(cachedImage == image) 228 | } else { 229 | XCTFail() 230 | } 231 | 232 | let data : Data! = string.data(using: .utf8) 233 | cache.setData(data, forKey: "data") 234 | if let cachedData = cache.data(forKey: "data") { 235 | XCTAssert(cachedData == data) 236 | } else { 237 | XCTFail() 238 | } 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /Cashier/Classes/Cashier.h: -------------------------------------------------------------------------------- 1 | // 2 | // Cashier.h 3 | // 4 | // 5 | // Created by Kasper Welner on 8/29/12. 6 | // Copyright (c) 2012 Nodes. All rights reserved. 7 | 8 | #import "TargetConditionals.h" 9 | #if TARGET_OS_IPHONE 10 | #import 11 | #else 12 | #import 13 | #endif 14 | static NSTimeInterval const CashierDateNotFound = 0; 15 | 16 | /** 17 | 18 | Cashier caches objects using key-value coding. 19 | 20 | Always use either: 21 | 22 | + (id)defaultCache 23 | 24 | or 25 | #import "TargetConditionals.h" 26 | + (id)cacheWithId:(NSString *)cacheID 27 | 28 | to get/create a cache. 29 | 30 | NOTE: If persistent caching is enabled (it is by default), 31 | you have to make sure that any collections you cache only contain 32 | objects conforming to the NSCoding protocol. 33 | 34 | Use the `lifespan` parameter to make time-limited caching. You can set it on a 35 | per-cache basis. 36 | */ 37 | @interface Cashier : NSObject 38 | 39 | #pragma mark - Properties 40 | 41 | /** 42 | * The id of the current Cashier object. 43 | */ 44 | @property (nonatomic, retain)NSString *id; 45 | 46 | 47 | /** 48 | * A boolean that determines if the current cache is persistent. 49 | * 50 | * If `persistent` is false, the objects will be cached in memory, using an NSCache. 51 | * 52 | * If `persistent` is true, the objects will be written to a file in the Library/Caches folder. 53 | * This means that its contents are not backed up by iTunes and may be deleted by the system. 54 | * Read more about the [iOS file system](https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) 55 | * 56 | * `persistent` is true by default. 57 | */ 58 | @property (atomic, assign)BOOL persistent; 59 | 60 | 61 | /** 62 | * A boolean that determines if the contents of the current cache are written to a protected file. 63 | * 64 | * If `encryptionEnabled` is false, the file that the cached objects are written to will be written using 65 | * [NSDataWritingFileProtectionComplete](https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSData_Class/index.html#//apple_ref/c/tdef/NSDataWritingOptions). 66 | * 67 | * If `encryptionEnabled` is false, the file that the cached objects are written to will not be protected 68 | * 69 | * `encryptionEnabled` is false by default. 70 | */ 71 | @property (atomic, assign)BOOL encryptionEnabled; 72 | 73 | /** 74 | * The lifespan of the current cache. If the cached objects are older than the cache's lifespan, they will 75 | * be considere expired. Expired objects are considered invalid, but they are not deleted. 76 | * Set the `returnsExpiredData` to true if you want to also use expired data. 77 | * `lifespan` is 0 by default, which means that the data won't ever expire. 78 | */ 79 | @property (atomic, assign)NSTimeInterval lifespan; 80 | 81 | /** 82 | * A boolean that determines if the contents of the current cache are returned even though they're expired. 83 | * `returnExpiredData` is true by default 84 | */ 85 | @property (atomic, assign)BOOL returnsExpiredData; 86 | 87 | 88 | /** 89 | * A boolean that determines if the contents of the current cache persist across app version updates. 90 | * `persistsCacheAcrossVersions` is false by default. This is because if a model changes, the app can crash 91 | * if it gets old data from the cache and it's expecting it to look differently. 92 | */ 93 | @property (nonatomic, assign)BOOL persistsCacheAcrossVersions; 94 | 95 | #pragma mark - Cache creation and access 96 | /** 97 | * Returns the default cache. This is the easiest way to create and access a cache. If the default cache wasn't 98 | * created before, this method creates it and returns it. Otherwise, it returns the previously created default cache. 99 | * 100 | * @return The default Cashier object 101 | */ 102 | + (instancetype)defaultCache; 103 | 104 | /** 105 | * Returns a cache object with the `cachedID` id. If the cache with the `cacheID` id wasn't created before, 106 | * this method creates it and returns it. 107 | * 108 | * @param cacheID the id of the cache 109 | * 110 | * @return A Cashier with the specified id. 111 | */ 112 | + (instancetype)cacheWithId:(NSString *)cacheID; 113 | 114 | #pragma mark - Adding data to cache 115 | 116 | /** 117 | * Sets the value of the specified key in the cache 118 | * 119 | * @warning For caching images, use the `setImage:forKey:` method. 120 | * @warning For caching NSData, use the `setData:forKey:` method. 121 | * @param object The object to be stored in the cache 122 | * @param key The key with which to associate the value 123 | */ 124 | - (void)setObject:(id)object forKey:(NSString *)key; 125 | 126 | #if TARGET_OS_IPHONE 127 | /** 128 | * Sets the image value of the specified key in the cache. 129 | * 130 | * @param image The UIImage to be stored in the cache 131 | * @param key The key with wich to associate the image 132 | */ 133 | - (void)setImage:(UIImage *)image forKey:(NSString *)key; 134 | #endif 135 | 136 | /** 137 | * Sets the data value of the specified key in the cache. 138 | * 139 | * @param data The NSData to be stored in the cache 140 | * @param key The key with wich to associate the image 141 | */ 142 | - (void)setData:(NSData *)data forKey:(NSString *)key; 143 | 144 | #pragma mark - Reading data from cache 145 | 146 | /** 147 | * Returns the value associated with a given key. 148 | * 149 | * @param key An object identifying the value. 150 | * 151 | * @return The value associated with key, or nil if no value is associated with key. 152 | */ 153 | - (id)objectForKey:(NSString *)key; 154 | 155 | #if TARGET_OS_IPHONE 156 | /** 157 | * Returns the image value associated with a given key. 158 | * 159 | * @param key An object identifying the value. 160 | * 161 | * @return The UIImage value associated with key, or nil if no value is associated with key. 162 | */ 163 | - (UIImage *)imageForKey:(NSString *)key; 164 | #endif 165 | 166 | /** 167 | * Returns the data value associated with a given key. 168 | * 169 | * @param key An object identifying the value. 170 | * 171 | * @return The NSData value associated with key, or nil if no value is associated with key. 172 | */ 173 | - (NSData *)dataForKey:(NSString *)key; 174 | 175 | 176 | /** 177 | * Returns the dictionary value associated with a given key. 178 | * 179 | * @param key An object identifying the value. 180 | * 181 | * @return The NSDictionary value associated with key, or nil if no value is associated with key. 182 | */ 183 | - (NSDictionary *)dictForKey:(NSString *)key; 184 | 185 | 186 | /** 187 | * Returns the array value associated with a given key. 188 | * 189 | * @param key An object identifying the value. 190 | * 191 | * @return The NSArray value associated with key, or nil if no value is associated with key. 192 | */ 193 | - (NSArray *)arrayForKey:(NSString *)key; 194 | 195 | 196 | /** 197 | * Returns the dictionary value associated with a given key. 198 | * 199 | * @param key An object identifying the value. 200 | * 201 | * @return The NSDictionary value associated with key, or nil if no value is associated with key. 202 | */ 203 | - (NSDictionary *)dictionaryForKey:(NSString *)key; 204 | 205 | 206 | /** 207 | * Returns the value associated with a given key. 208 | * 209 | * @param key An object identifying the value. 210 | * 211 | * @return The value associated with key, or nil if no value is associated with key. 212 | */ 213 | - (NSString *)stringForKey:(NSString *)key; 214 | 215 | #pragma mark - Removing data from cache 216 | 217 | /** 218 | * Clears all the data from the cache. 219 | */ 220 | - (void)clearAllData; 221 | 222 | 223 | /** 224 | * Removes the value associated with the given key. 225 | * 226 | * @param key An object identifying the value that will be removed. 227 | */ 228 | - (void)deleteObjectForKey:(NSString *)key; 229 | 230 | #pragma mark - Cached data validation 231 | 232 | /** 233 | * Checks if an object for the specified key is valid. This means that the object exists in 234 | * the cache and is not expired 235 | * 236 | * @param key An object identifying the value. 237 | * 238 | * @return `YES` if an object exists for the specified key and the object isn't expired. `NO` oherwise. 239 | */ 240 | - (BOOL)objectForKeyIsValid:(NSString *)key; 241 | 242 | 243 | /** 244 | * Updates the validation timestamp to current timestamp for the object at the specified key. 245 | * 246 | * @param key An object identifying the value that will have its validation timestamp updated 247 | */ 248 | - (void)refreshValidationOnObjectForKey:(NSString *)key; 249 | 250 | 251 | /** 252 | * Returns the current validation timestamp of the object at the specified key. This is the timestamp 253 | * when the object was last updated in cache or it was explicitly re-validated using the 254 | * `refershValidationOnObjectForKey:` method 255 | * 256 | * @param key An object identifying the value 257 | * 258 | * @return The timestamp of the last update of the object corresponding to the specified key, 259 | * as the `timeIntervalSince1970` for the date when the object was last updated. 260 | */ 261 | - (NSTimeInterval)lastUpdateTimeForKey:(NSString *)key; 262 | 263 | 264 | 265 | @end 266 | -------------------------------------------------------------------------------- /CashierTests/CashierTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // CashierTests.m 3 | // CashierTests 4 | // 5 | // Created by Chris Combs on 09/02/16. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "Cashier.h" 11 | #import 12 | #import "NSBundle+Swizzle.h" 13 | #import "NSMutableDictionary+Swizzle.h" 14 | 15 | @interface CashierTests : XCTestCase 16 | 17 | @end 18 | 19 | @implementation CashierTests 20 | 21 | + (void)load { 22 | static dispatch_once_t onceToken; 23 | dispatch_once(&onceToken, ^{ 24 | Class class = [[NSBundle mainBundle] class]; 25 | 26 | SEL originalSelector = @selector(objectForInfoDictionaryKey:); 27 | SEL swizzledSelector = @selector(szl_objectForInfoDictionaryKey:); 28 | 29 | Method originalMethod = class_getInstanceMethod(class, originalSelector); 30 | Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 31 | 32 | BOOL didAddMethod = 33 | class_addMethod(class, 34 | originalSelector, 35 | method_getImplementation(swizzledMethod), 36 | method_getTypeEncoding(swizzledMethod)); 37 | 38 | if (didAddMethod) { 39 | class_replaceMethod(class, 40 | swizzledSelector, 41 | method_getImplementation(originalMethod), 42 | method_getTypeEncoding(originalMethod)); 43 | } else { 44 | method_exchangeImplementations(originalMethod, swizzledMethod); 45 | } 46 | 47 | 48 | 49 | 50 | Class class2 = [[[NSMutableDictionary alloc] init] class]; 51 | 52 | SEL originalSelector2 = @selector(objectForKey:); 53 | SEL swizzledSelector2 = @selector(szl_objectForKey:); 54 | 55 | Method originalMethod2 = class_getInstanceMethod(class2, originalSelector2); 56 | Method swizzledMethod2 = class_getInstanceMethod(class2, swizzledSelector2); 57 | 58 | BOOL didAddMethod2 = 59 | class_addMethod(class2, 60 | originalSelector2, 61 | method_getImplementation(swizzledMethod2), 62 | method_getTypeEncoding(swizzledMethod2)); 63 | 64 | if (didAddMethod2) { 65 | class_replaceMethod(class2, 66 | swizzledSelector2, 67 | method_getImplementation(originalMethod2), 68 | method_getTypeEncoding(originalMethod2)); 69 | } else { 70 | method_exchangeImplementations(originalMethod2, swizzledMethod2); 71 | } 72 | }); 73 | 74 | } 75 | 76 | 77 | 78 | - (void)setUp { 79 | [super setUp]; 80 | // Put setup code here. This method is called before the invocation of each test method in the class. 81 | } 82 | 83 | - (void)tearDown { 84 | // Put teardown code here. This method is called after the invocation of each test method in the class. 85 | [super tearDown]; 86 | } 87 | 88 | - (void)testObjectCachingInDefaultCache { 89 | Cashier* cashier = Cashier.defaultCache; 90 | 91 | NSString* stringToCache = @"this will be cached"; 92 | NSString* stringKey = @"stringkey"; 93 | 94 | [cashier setObject: stringToCache forKey:stringKey]; 95 | NSString* stringFromCache = [cashier objectForKey:stringKey]; 96 | 97 | XCTAssertEqual(stringFromCache, stringToCache); 98 | 99 | NSString* sameStringFromCache = [cashier stringForKey:stringKey]; 100 | XCTAssertEqual(sameStringFromCache, stringToCache); 101 | 102 | [cashier deleteObjectForKey:stringKey]; 103 | NSString* noStringFromCache = [cashier objectForKey:stringKey]; 104 | XCTAssertNil(noStringFromCache); 105 | 106 | NSDictionary* dictToCache = [[NSDictionary alloc] initWithObjects:@[@"obj"] forKeys:@[@"key"]]; 107 | NSString* dictKey = @"dictkey"; 108 | 109 | [cashier setObject: dictToCache forKey:dictKey]; 110 | NSDictionary* dictFromCache = [cashier objectForKey:dictKey]; 111 | 112 | XCTAssertEqual(dictToCache, dictFromCache); 113 | 114 | NSDictionary* sameDictFromCache = [cashier dictForKey:dictKey]; 115 | XCTAssertEqual(dictToCache, sameDictFromCache); 116 | 117 | NSDictionary* againSameDictFromCache = [cashier dictionaryForKey:dictKey]; 118 | XCTAssertEqual(dictToCache, againSameDictFromCache); 119 | 120 | [cashier deleteObjectForKey:dictKey]; 121 | NSDictionary* noDictFromCache = [cashier objectForKey:dictKey]; 122 | XCTAssertNil(noDictFromCache); 123 | 124 | NSData* dataToCache = [[NSData alloc] initWithBase64EncodedString:stringToCache options:NSDataBase64DecodingIgnoreUnknownCharacters]; 125 | NSString* dataKey = @"dataKey"; 126 | 127 | [cashier setObject:dataToCache forKey:dataKey]; 128 | NSData* dataFromCache = [cashier objectForKey:dataKey]; 129 | 130 | XCTAssertEqual(dataToCache, dataFromCache); 131 | 132 | NSData* sameDataFromCache = [cashier dataForKey:dataKey]; 133 | XCTAssertEqual(dataToCache, sameDataFromCache); 134 | 135 | [cashier deleteObjectForKey:dataKey]; 136 | NSData* noDataFromCache = [cashier objectForKey:dataKey]; 137 | XCTAssertNil(noDataFromCache); 138 | 139 | NSArray* arrayToCache = [[NSArray alloc] initWithObjects:@"this", @"will", @"be", @"cached", nil]; 140 | NSString* arrayKey = @"arrayKey"; 141 | 142 | [cashier setObject:arrayToCache forKey:arrayKey]; 143 | NSArray* arrayFromCache = [cashier objectForKey:arrayKey]; 144 | 145 | XCTAssertEqual(arrayToCache, arrayFromCache); 146 | 147 | NSArray* sameArrayFromCache = [cashier arrayForKey:arrayKey]; 148 | XCTAssertEqual(arrayToCache, sameArrayFromCache); 149 | 150 | [cashier deleteObjectForKey:arrayKey]; 151 | NSArray* noArrayFromCache = [cashier objectForKey:arrayKey]; 152 | XCTAssertNil(noArrayFromCache); 153 | #if TARGET_OS_IPHONE 154 | UIImage* imageToCache = [UIImage imageNamed:@"Nodes" inBundle:[NSBundle bundleForClass:self.class] compatibleWithTraitCollection:nil]; 155 | XCTAssertNotNil(imageToCache); 156 | NSString* imageKey = @"imageKey"; 157 | 158 | [cashier setImage:imageToCache forKey:imageKey]; 159 | UIImage* imageFromCache = [cashier objectForKey:imageKey]; 160 | 161 | XCTAssertEqual(imageToCache, imageFromCache); 162 | 163 | UIImage* sameImageFromCache = [cashier imageForKey:imageKey]; 164 | XCTAssertEqual(imageToCache, sameImageFromCache); 165 | 166 | [cashier deleteObjectForKey:imageKey]; 167 | UIImage* noImageFromCache = [cashier objectForKey:imageKey]; 168 | XCTAssertNil(noImageFromCache); 169 | #endif 170 | 171 | } 172 | 173 | - (void)testCacheClearingInDefaultCache { 174 | Cashier* cashier = Cashier.defaultCache; 175 | 176 | NSString* stringToCache = @"this will be cached"; 177 | NSString* stringKey = @"stringkey"; 178 | [cashier setObject: stringToCache forKey:stringKey]; 179 | 180 | 181 | NSDictionary* dictToCache = [[NSDictionary alloc] initWithObjects:@[@"obj"] forKeys:@[@"key"]]; 182 | NSString* dictKey = @"dictkey"; 183 | [cashier setObject: dictToCache forKey:dictKey]; 184 | 185 | NSData* dataToCache = [[NSData alloc] initWithBase64EncodedString:stringToCache options:NSDataBase64DecodingIgnoreUnknownCharacters]; 186 | NSString* dataKey = @"dataKey"; 187 | [cashier setObject:dataToCache forKey:dataKey]; 188 | 189 | NSArray* arrayToCache = [[NSArray alloc] initWithObjects:@"this", @"will", @"be", @"cached", nil]; 190 | NSString* arrayKey = @"arrayKey"; 191 | [cashier setObject:arrayToCache forKey:arrayKey]; 192 | #if TARGET_OS_IPHONE 193 | UIImage* imageToCache = [UIImage imageNamed:@"Nodes" inBundle:[NSBundle bundleForClass:self.class] compatibleWithTraitCollection:nil]; 194 | XCTAssertNotNil(imageToCache); 195 | NSString* imageKey = @"imageKey"; 196 | [cashier setImage:imageToCache forKey:imageKey]; 197 | #endif 198 | 199 | [cashier clearAllData]; 200 | 201 | NSString* noStringFromCache = [cashier objectForKey:stringKey]; 202 | XCTAssertFalse([cashier objectForKeyIsValid:stringKey]); 203 | XCTAssertNil(noStringFromCache); 204 | 205 | 206 | NSDictionary* noDictFromCache = [cashier objectForKey:dictKey]; 207 | XCTAssertFalse([cashier objectForKeyIsValid:dictKey]); 208 | XCTAssertNil(noDictFromCache); 209 | 210 | 211 | NSData* noDataFromCache = [cashier objectForKey:dataKey]; 212 | XCTAssertFalse([cashier objectForKeyIsValid:dataKey]); 213 | XCTAssertNil(noDataFromCache); 214 | 215 | 216 | NSArray* noArrayFromCache = [cashier objectForKey:arrayKey]; 217 | XCTAssertFalse([cashier objectForKeyIsValid:arrayKey]); 218 | XCTAssertNil(noArrayFromCache); 219 | 220 | #if TARGET_OS_IPHONE 221 | UIImage* noImageFromCache = [cashier objectForKey:imageKey]; 222 | XCTAssertFalse([cashier objectForKeyIsValid:imageKey]); 223 | XCTAssertNil(noImageFromCache); 224 | #endif 225 | 226 | } 227 | 228 | - (void)testCacheLifespanObjectExpired { 229 | Cashier* cashier = Cashier.defaultCache; 230 | cashier.lifespan = 0.5; 231 | cashier.returnsExpiredData = YES; 232 | NSString* stringToCache = @"this will be cached"; 233 | NSString* stringKey = @"stringkey"; 234 | [cashier setObject: stringToCache forKey:stringKey]; 235 | NSString* stringFromCache = [cashier objectForKey:stringKey]; 236 | XCTAssertEqual(stringFromCache, stringToCache); 237 | 238 | XCTestExpectation* expectation = [self expectationWithDescription:@"expecting cached string object to be invalid"]; 239 | 240 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 241 | XCTAssertFalse([cashier objectForKeyIsValid:stringKey]); 242 | XCTAssertNotNil([cashier objectForKey:stringKey]); 243 | [expectation fulfill]; 244 | }); 245 | [self waitForExpectationsWithTimeout:1 handler:nil]; 246 | 247 | } 248 | 249 | - (void)testCacheLifespanObjectBecomesNil { 250 | Cashier* cashier = Cashier.defaultCache; 251 | cashier.lifespan = 0.5; 252 | cashier.returnsExpiredData = NO; 253 | NSString* stringToCache = @"this will be cached"; 254 | NSString* stringKey = @"stringkey"; 255 | [cashier setObject: stringToCache forKey:stringKey]; 256 | NSString* stringFromCache = [cashier objectForKey:stringKey]; 257 | XCTAssertEqual(stringFromCache, stringToCache); 258 | 259 | XCTestExpectation* expectation = [self expectationWithDescription:@"expecting cached string object to be invalid"]; 260 | 261 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 262 | XCTAssertFalse([cashier objectForKeyIsValid:stringKey]); 263 | XCTAssertNil([cashier objectForKey:stringKey]); 264 | [expectation fulfill]; 265 | }); 266 | [self waitForExpectationsWithTimeout:1 handler:nil]; 267 | 268 | } 269 | #if TARGET_OS_IPHONE 270 | - (void)testMemoryCacheClearsOnMemoryWarning { 271 | Cashier* cashier = Cashier.defaultCache; 272 | cashier.persistent = NO; 273 | 274 | NSString* stringToCache = @"this will be cached"; 275 | NSString* stringKey = @"stringkey"; 276 | [cashier setObject: stringToCache forKey:stringKey]; 277 | NSString* stringFromCache = [cashier objectForKey:stringKey]; 278 | XCTAssertEqual(stringFromCache, stringToCache); 279 | 280 | [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 281 | 282 | XCTAssertNil([cashier objectForKey:stringKey]); 283 | 284 | } 285 | #endif 286 | 287 | - (void)testCachePersistsOnVersionUpdates { 288 | NSString* cacheID = @"szl_cacheID1"; 289 | Cashier* cashier = [Cashier cacheWithId:cacheID]; 290 | cashier.persistsCacheAcrossVersions = YES; 291 | 292 | 293 | 294 | NSString* stringToCache = @"this will be cached"; 295 | NSString* stringKey = @"stringkey"; 296 | [cashier setObject: stringToCache forKey:stringKey]; 297 | 298 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 299 | [userDefaults setObject:@"0.0.1" forKey:[@"NOCACHE_VER_" stringByAppendingString:cacheID]]; 300 | [userDefaults synchronize]; 301 | 302 | Cashier* cashier2 = [Cashier cacheWithId:cacheID]; 303 | 304 | NSString* stringFromCache = [cashier2 objectForKey:stringKey]; 305 | 306 | XCTAssertEqual(stringFromCache, stringToCache); 307 | XCTAssertNotNil([cashier2 objectForKey:stringKey]); 308 | 309 | } 310 | 311 | 312 | - (void)testCacheDoesNotPersistOnVersionUpdates { 313 | NSString* cacheID = @"szl_cacheID2"; 314 | 315 | Cashier* cashier = [Cashier cacheWithId:cacheID]; 316 | cashier.persistsCacheAcrossVersions = NO; 317 | 318 | NSString* stringToCache = @"this will be cached"; 319 | NSString* stringKey = @"stringkey"; 320 | [cashier setObject: stringToCache forKey:stringKey]; 321 | 322 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 323 | [userDefaults setObject:@"0.0.1" forKey:[@"NOCACHE_VER_" stringByAppendingString:cacheID]]; 324 | [userDefaults synchronize]; 325 | 326 | Cashier* cashier2 = [Cashier cacheWithId:cacheID]; 327 | 328 | NSString* stringFromCache = [cashier2 objectForKey:stringKey]; 329 | 330 | XCTAssertFalse([cashier2 objectForKeyIsValid:stringKey], @"Object is valid even though it shouldn't be in a new version."); 331 | XCTAssertNil(stringFromCache); 332 | } 333 | 334 | 335 | - (void)testCacheActuallySavesOnDisk { 336 | Cashier* cashier = Cashier.defaultCache; 337 | // forcing cache to write to files 338 | cashier.persistent = YES; 339 | 340 | NSString* stringToCache = @"this will be cached"; 341 | NSString* stringKey = @"stringkey"; 342 | [cashier setObject: stringToCache forKey:stringKey]; 343 | NSString* stringFromCache = [cashier objectForKey:stringKey]; 344 | XCTAssertEqual(stringFromCache, stringToCache); 345 | #if TARGET_OS_IPHONE 346 | // forcing cache to clear memory cache 347 | [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 348 | #endif 349 | XCTAssertNotNil([cashier objectForKey:stringKey]); 350 | XCTAssertTrue([stringToCache isEqualToString:[cashier objectForKey:stringKey]]); 351 | 352 | } 353 | 354 | @end 355 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This library has been deprecated and the repo has been archived. 2 | ### The code is still here and you can still clone it, however the library will not receive any more updates or support. 3 | 4 |

5 | Cashier 6 |

7 | 8 | Cashier is a caching framework that makes it easy to work with persistent data. 9 | 10 | [![CircleCI](https://circleci.com/gh/nodes-ios/Cashier.svg?style=shield)](https://circleci.com/gh/nodes-ios/Cashier) 11 | [![Codecov](https://img.shields.io/codecov/c/github/nodes-ios/Cashier.svg)](https://codecov.io/github/nodes-ios/Cashier) 12 | [![CocoaPods](https://img.shields.io/cocoapods/v/Cashier.svg)](https://cocoapods.org/pods/Cashier) 13 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 14 | ![Plaform](https://img.shields.io/badge/platform-iOS-lightgrey.svg) 15 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nodes-ios/Codemine/blob/master/LICENSE) 16 | [![Readme Score](http://readme-score-api.herokuapp.com/score.svg?url=nodes-ios/cashier)](http://clayallsopp.github.io/readme-score?url=nodes-ios/cashier) 17 | 18 | ## 📝 Requirements 19 | 20 | * iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ 21 | * Xcode 8.1+ 22 | 23 | ## 📦 Installation 24 | 25 | ### Carthage 26 | ~~~ 27 | github "nodes-ios/Cashier" 28 | ~~~ 29 | 30 | ### CocoaPods 31 | 32 | ~~~ 33 | pod 'Cashier', '~> 1.0.1' 34 | ~~~ 35 | 36 | ## 💻 Usage 37 | #### Creating and accessing a Cashier object 38 | *Swift* 39 | ```swift 40 | // Create a Cashier object with id "cacheID". 41 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 42 | 43 | // Create a Cashier object the default cache. 44 | let cashier: Cashier = Cashier.defaultCache() 45 | ``` 46 | 47 | *Objective-C* 48 | ```objective-c 49 | // Create a Cashier object with id "cacheID". 50 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 51 | 52 | // Create a Cashier object the default cache. 53 | Cashier* defaultCashier = Cashier.defaultCache; 54 | ``` 55 | 56 | The same method is used both for creating and accessing a Cashier object. If the Cashier object with the specified id hasn't been created before, it will be created and returned. If it has already been created, the existing one will be returned. 57 | 58 | #### Caching NSString in a Cashier object 59 | *Swift* 60 | ```swift 61 | // Get or create a Cashier object with id "stringCache". 62 | let cashier: Cashier = Cashier.cache(withId: "stringCache") 63 | 64 | // Add an object to the cache. 65 | cashier.setObject(stringToCache, forKey: "string") 66 | 67 | // Get the object from the cache. 68 | let cachedString: String = cashier.object(forKey: "string") 69 | ``` 70 | 71 | *Objective-C* 72 | ```objective-c 73 | // Get or create a Cashier object with id "stringCache". 74 | Cashier* cashier = [Cashier cacheWithId:@"stringCache"]; 75 | 76 | // Add an object to the cache. 77 | [cashier setObject: stringToCache forKey:@"stringCacheKey"]; 78 | 79 | // Get the object from the cache. 80 | NSString* stringFromCache = [cashier objectForKey: @"stringCacheKey"]; 81 | ``` 82 | 83 | #### Caching NSData in a Cashier object 84 | *Swift* 85 | ```swift 86 | // Get or create a Cashier object with id "dataCache". 87 | let cashier: Cashier = Cashier.cache(withId: "dataCache") 88 | 89 | // Add an object to the cache. 90 | cashier.setData(yourNSDataObject, forKey: "dataCacheKey") 91 | 92 | // Get the object from the cache. 93 | let cachedData = cashier.data(forKey: "dataCacheKey") 94 | ``` 95 | 96 | *Objective-C* 97 | ```objective-c 98 | // Get or create a Cashier object with id "dataCache". 99 | Cashier* cashier = [Cashier cacheWithId:@"dataCache"]; 100 | 101 | // Add an object to the cache. 102 | [cashier setData: yourNSDataObject forKey:@"dataCacheKey"]; 103 | 104 | // Get the object from the cache. 105 | NSData* dataFromCache = [cashier dataForKey:@"dataCacheKey"]; 106 | ``` 107 | 108 | #### Caching UIImage in a Cashier object 109 | *Swift* 110 | ```swift 111 | // Get or create a Cashier object with id "imageCache". 112 | let cashier: Cashier = Cashier.cache(withId: "imageCache") 113 | 114 | // Add an object to the cache. 115 | cashier.setImage(yourUIImage, forKey: "imageCacheKey") 116 | 117 | // Get the object from the cache. 118 | let imageFromCache: UIImage = cashier.image(forKey: "imageCacheKey") 119 | ``` 120 | 121 | *Objective-C* 122 | ```objective-c 123 | // Get or create a Cashier object with id "imageCache". 124 | Cashier* cashier = [Cashier cacheWithId:@"imageCache"]; 125 | 126 | // Add an object to the cache. 127 | [cashier setImage: yourUIImage forKey:@"imageCacheKey"]; 128 | 129 | // Get the object from the cache. 130 | UIImage* imageFromCache = [cashier imageForKey:@"imageCacheKey"]; 131 | ``` 132 | 133 | #### Caching any other type of objects (including custom objects) 134 | *Swift* 135 | ```swift 136 | // Get or create a Cashier object with id "cacheID". 137 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 138 | 139 | // Add an object to the cache. 140 | let yourObject: YourObject = YourObject() 141 | cashier.setObject(yourObject, forKey: "cacheKey") 142 | 143 | // Get the object from the cache. 144 | let objectFromCache: YourObject = cashier.object(forKey: "cacheKey") 145 | ``` 146 | 147 | *Objective-C* 148 | ```objective-c 149 | // Get or create a Cashier object with id "cacheID". 150 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 151 | 152 | // Add an object to the cache. 153 | YourObject *yourObject = [[YourObject alloc] init]; 154 | [cashier setObject: yourObject forKey:@"cacheKey"]; 155 | 156 | // Get the object from the cache. 157 | YourObject* stringFromCache = [cashier objectForKey:@"cacheKey"]; 158 | ``` 159 | 160 | You can get typed objects from the cache using the following methods: 161 | 162 | *Swift* 163 | ```swift 164 | - image(forKey: String) 165 | - data(forKey: String) 166 | - dictionary(forKey: String) 167 | - array(forKey: String) 168 | - string(forKey: String) 169 | ``` 170 | 171 | *Objective-C* 172 | ```objective-c 173 | - (UIImage *)imageForKey:(NSString *)key; 174 | - (NSData *)dataForKey:(NSString *)key; 175 | - (NSDictionary *)dictionaryForKey:(NSString *)key; 176 | - (NSArray *)arrayForKey:(NSString *)key; 177 | - (NSString *)stringForKey:(NSString *)key; 178 | ``` 179 | 180 | #### Persistency 181 | 182 | A cashier object has 2 layers of cache. If the `persistent` property is set to `NO`, the objects will be cached in memory, using an NSCache. If `persistent` is `YES`, the objects will be written to a file in the `Library/Caches` folder. This means that its contents are not backed up by iTunes and may be deleted by the system. Read more about the [iOS file system](https://developer.apple.com/library/ios/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html). 183 | 184 | *Swift* 185 | ```swift 186 | // Get or create a Cashier object with id "cacheID". 187 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 188 | 189 | // Makes the Cashier cache the objects in files. 190 | cashier.persistent = true 191 | 192 | // Makes the Cashier cache the objects in memory. 193 | cashier.persistent = false 194 | ``` 195 | 196 | *Objective-C* 197 | 198 | ```objective-c 199 | // Get or create a Cashier object with id "cacheID". 200 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 201 | 202 | // Makes the Cashier cache the objects in files. 203 | cashier.persistent = YES; 204 | 205 | // Makes the Cashier cache the objects in memory. 206 | cashier.persistent = NO; 207 | ``` 208 | 209 | For persistent data that isn't saved in the `Library/Caches` folder, use the `NOPersistentStore`. This is a subclass of `Cashier`, that works exactly the same way as `Cashier`, but it saves its data to the `Documents/NOCache` folder, which isn't cleared by the system when the device runs out of space. 210 | 211 | *Swift* 212 | ```swift 213 | // Get or create a an NOPersistentStore object with id "noPersistentStoreId". 214 | let noPersistentStore: NOPersistentStore = NOPersistentStore.cache(withId: "noPersistentStoreId") 215 | ``` 216 | 217 | *Objective-C* 218 | ```objective-c 219 | // Get or create a an NOPersistentStore object with id "noPersistentStoreId". 220 | NOPersistentStore* noPersistentStore = [NOPersistentStore cacheWithId:@"persistentStoreId"]; 221 | ``` 222 | 223 | ### Expiration 224 | 225 | A `Cashier` object has a `lifespan`. This determine how long the cached objects should be valid before they're considered expired. If the cached objects are older than the cache’s lifespan, they will be considered expired. Expired objects are considered invalid, but they are not deleted. Set the `returnsExpiredData` to `YES` if you want to also use expired data. `lifespan` is 0 by default, which means that the data will never expire. 226 | 227 | *Swift* 228 | ```swift 229 | // Get or create a Cashier object with id "cacheID". 230 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 231 | 232 | // Makes the cached objects have a lifespan of one minute. 233 | cashier.lifespan = 60 234 | 235 | // Makes the cache not return cached objects after their lifespan has passed. 236 | cashier.returnsExpiredData = false 237 | 238 | let yourObject: YourObject = YourObject() 239 | cashier.setObject(yourObject, forKey: "cacheKey") 240 | 241 | // After one minute passes. 242 | 243 | cache.object(forKeyIsValid: "cacheKey") 244 | // Returns: false 245 | 246 | cache.object(forKey: "cacheKey") 247 | // Returns: nil 248 | ``` 249 | 250 | ```swift 251 | // Get or create a Cashier object with id "cacheID". 252 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 253 | 254 | // Makes the cached objects have a lifespan of one minute. 255 | cashier.lifespan = 60 256 | 257 | // Makes the cache return cached objects after their lifespan has passed. 258 | cashier.returnsExpiredData = true 259 | 260 | let yourObject: YourObject = YourObject() 261 | cashier.setObject(yourObject, forKey: "cacheKey") 262 | 263 | // After one minute passes. 264 | 265 | cache.object(forKeyIsValid: "cacheKey") 266 | // Returns: false 267 | 268 | cache.object(forKey: "cacheKey") 269 | // Returns: yourObject 270 | ``` 271 | 272 | *Objective-C* 273 | ```objective-c 274 | // Get or create a Cashier object with id "cacheID". 275 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 276 | 277 | // Makes the cached objects have a lifespan of one minute. 278 | cashier.lifespan = 60; 279 | 280 | // Makes the cache not return cached objects after their lifespan has passed. 281 | cashier.returnsExpiredData = NO; 282 | 283 | YourObject *yourObject = [[YourObject alloc] init]; 284 | [cashier setObject: yourObject forKey:@"cacheKey"]; 285 | 286 | // After one minute passes. 287 | 288 | [cashier objectForKeyIsValid:@"cacheKey"] 289 | // Returns: NO 290 | 291 | [cashier objectForKey:@"cacheKey"] 292 | // Returns: nil 293 | ``` 294 | 295 | ```objective-c 296 | // Get or create a Cashier object with id "cacheID". 297 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 298 | 299 | // Makes the cached objects have a lifespan of one minute. 300 | cashier.lifespan = 60; 301 | 302 | // Makes the cache return cached objects after their lifespan has passed. 303 | cashier.returnsExpiredData = YES; 304 | 305 | YourObject *yourObject = [[YourObject alloc] init]; 306 | [cashier setObject: yourObject forKey:@"cacheKey"]; 307 | 308 | // After one minute passes. 309 | 310 | [cashier objectForKeyIsValid:@"cacheKey"] 311 | // Returns: NO 312 | 313 | [cashier objectForKey:@"cacheKey"] 314 | // Returns: yourObject 315 | ``` 316 | 317 | It's possible to refresh the validation timestamp of an object using the `- (void)refreshValidationOnObjectForKey:(NSString *)key;` method. This will update the timestamp when the cached object was last validated to the current timestamp. This means that if an object had a lifespan of 1 minute, but then we refresh its validation, it will have a lifespan of 1 minute from the moment we refreshed its validation. 318 | 319 | *Swift* 320 | ```swift 321 | // Get or create a Cashier object with id "cacheID". 322 | let cashier: Cashier = Cashier.cache(withId: "cacheID") 323 | 324 | // Makes the cached objects have a lifespan of one minute. 325 | cashier.lifespan = 60 326 | 327 | let yourObject: YourObject = YourObject() 328 | cashier.setObject(yourObject, forKey: "cacheKey") 329 | 330 | // After 30 seconds passed 331 | 332 | cache.refreshValidationOnObject(forKey: "cacheKey") 333 | 334 | // After another 40 seconds passed. 335 | 336 | cache.object(forKeyIsValid: "cacheKey") 337 | // Returns: true 338 | ``` 339 | 340 | *Objective-C* 341 | ```objective-c 342 | // Get or create a Cashier object with id "cacheID". 343 | Cashier* cashier = [Cashier cacheWithId:@"cacheID"]; 344 | 345 | // Makes the cached objects have a lifespan of one minute. 346 | cashier.lifespan = 60; 347 | 348 | YourObject *yourObject = [[YourObject alloc] init]; 349 | [cashier setObject: yourObject forKey:@"cacheKey"]; 350 | 351 | // After 30 seconds passed 352 | 353 | [cashier refreshValidationOnObjectForKey:@"cacheKey"]; 354 | 355 | // After another 40 seconds passed. 356 | 357 | [cashier objectForKeyIsValid:@"cacheKey"] 358 | // Returns: YES 359 | ``` 360 | 361 | The `persistsCacheAcrossVersions` property determines if the contents of the current cache persist across app version updates. `persistsCacheAcrossVersions` is `false` by default. This is because if a model changes, the app can crash if it gets old data from the cache and it’s expecting it to look differently. 362 | 363 | ### Security 364 | 365 | The `encryptionEnabled` determines if the contents of the current cache are written to a protected file. If `encryptionEnabled` is false, the file that the cached objects are written to will be written using 366 | [NSDataWritingFileProtectionComplete](https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSData_Class/index.html#//apple_ref/c/tdef/NSDataWritingOptions). 367 | If `encryptionEnabled` is false, the file that the cached objects are written to will not be protected. `encryptionEnabled` is false by default. 368 | 369 | ### Usage guidelines 370 | 371 | * We recommend using different caches for different types of data. 372 | * Use the `Cashier.defaultCache` to get very easy access to a cache. 373 | * We don't recommend using the default cache for storing lots of data (f.x., a data source of a table view), but for singular properties or objects. Also take into account the fact that the default cache doesn't persist across version updates. 374 | 375 | 376 | ## 👥 Credits 377 | Made with ❤️ at [Nodes](http://nodesagency.com). 378 | 379 | ## 📄 License 380 | **Cashier** is available under the MIT license. See the [LICENSE](https://github.com/nodes-ios/Cashier/blob/master/LICENSE) file for more info. 381 | -------------------------------------------------------------------------------- /Cashier/Classes/Cashier.m: -------------------------------------------------------------------------------- 1 | // 2 | // Cashier.m 3 | // 4 | // 5 | // Created by Kasper Welner on 8/29/12. 6 | // Copyright (c) 2012 Nodes. All rights reserved. 7 | // 8 | 9 | #import "Cashier.h" 10 | #import "NSString+MD5Addition.h" 11 | 12 | @interface Cashier() 13 | @property (atomic, strong)NSMutableDictionary *creationDates; 14 | @property (nonatomic, retain)NSString *storagePath; 15 | @property (nonatomic, strong)NSCache *memoryCache; 16 | @end 17 | 18 | static NSMutableDictionary *sharedInstances; 19 | static NSString * const kCreationDatesKey = @"CreationDate"; 20 | static NSString * const kPropertiesKey = @"Properties"; 21 | 22 | @implementation Cashier 23 | { 24 | NSMutableDictionary *properties; 25 | } 26 | 27 | - (id)init 28 | { 29 | self = [super init]; 30 | if (self) 31 | { 32 | [self setStoragePath:[self.class baseSaveDirectory]]; 33 | self.memoryCache = [[NSCache alloc] init]; 34 | self.persistent = YES; 35 | self.returnsExpiredData = YES; 36 | 37 | #if SUPPORTS_MEMORY_WARNING 38 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(emptyCache) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 39 | #endif 40 | } 41 | 42 | return self; 43 | } 44 | 45 | - (void)emptyCache 46 | { 47 | if( self.memoryCache ) { 48 | NSLog(@"CASHIER REMOVING MEMORY"); 49 | [self.memoryCache removeAllObjects]; 50 | } 51 | } 52 | 53 | - (void)dealloc 54 | { 55 | [self removeKVO]; 56 | #if SUPPORTS_MEMORY_WARNING 57 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 58 | #endif 59 | } 60 | 61 | + (NSString *)baseSaveDirectory 62 | { 63 | return [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"Cashier"]; 64 | } 65 | 66 | 67 | + (instancetype)defaultCache 68 | { 69 | return [self cacheWithId:@"INTERNAL_DEFAULT_CACHE"]; 70 | } 71 | 72 | + (instancetype)cacheWithId:(NSString *)cacheID 73 | { 74 | if (cacheID == nil) 75 | return nil; 76 | 77 | static dispatch_once_t onceToken; 78 | dispatch_once(&onceToken, ^{ 79 | sharedInstances = [NSMutableDictionary dictionaryWithCapacity:2]; 80 | }); 81 | 82 | Cashier *requestedCacheInstance; 83 | 84 | @synchronized (self) 85 | { 86 | requestedCacheInstance = [sharedInstances objectForKey:cacheID]; 87 | 88 | if (requestedCacheInstance == nil) 89 | { 90 | requestedCacheInstance = [[self alloc] init]; 91 | requestedCacheInstance.id = cacheID; 92 | 93 | NSString *idDir = [requestedCacheInstance encodedStringForUseInPath:requestedCacheInstance.id]; 94 | NSString *dirPath = [requestedCacheInstance.storagePath stringByAppendingPathComponent:idDir]; 95 | 96 | NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; 97 | NSString *cacheVersion = [userDefaults objectForKey:[@"NOCACHE_VER_" stringByAppendingString:cacheID]]; 98 | NSString *appCurrentVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 99 | 100 | if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) 101 | { 102 | [self createDirectoryAtPath:dirPath]; 103 | [requestedCacheInstance updateSystemVersion]; 104 | 105 | } else if ( cacheVersion != nil && [self isVersion:appCurrentVersion greaterThanVersion:cacheVersion] ) { 106 | 107 | NSNumber *savedPersistsCacheAcrossVersions = [[NSUserDefaults standardUserDefaults] objectForKey:[@"NOCACHE_PERSISTS_VER_" stringByAppendingString:cacheID]]; 108 | if (savedPersistsCacheAcrossVersions != nil) { 109 | [requestedCacheInstance setValue:savedPersistsCacheAcrossVersions forKey:@"persistsCacheAcrossVersions"]; 110 | } 111 | 112 | if ( requestedCacheInstance.persistsCacheAcrossVersions == NO ) { 113 | NSError *error; 114 | 115 | [[NSFileManager defaultManager] removeItemAtPath:dirPath error:&error]; 116 | if (error != nil) { 117 | NSLog(@"Error! Couldn't delete directory: %@ (deletion because system version increased) ", error.localizedDescription); 118 | } else { 119 | NSLog(@"Deleting cache because version number incremented"); 120 | } 121 | 122 | [self createDirectoryAtPath:dirPath]; 123 | [requestedCacheInstance updateSystemVersion]; 124 | } 125 | } 126 | 127 | [requestedCacheInstance loadCreationDates]; 128 | [requestedCacheInstance loadProperties]; 129 | [requestedCacheInstance addKVO]; 130 | [sharedInstances setObject:requestedCacheInstance forKey:cacheID]; 131 | 132 | 133 | } 134 | } 135 | 136 | return requestedCacheInstance; 137 | } 138 | 139 | + (void)createDirectoryAtPath:(NSString *)path 140 | { 141 | NSError *error; 142 | 143 | [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error]; 144 | if (error != nil) { 145 | NSLog(@"Error! Couldn't create directory: %@", error.localizedDescription); 146 | } 147 | } 148 | 149 | - (void)updateSystemVersion 150 | { 151 | [[NSUserDefaults standardUserDefaults] setObject:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] forKey:[@"NOCACHE_VER_" stringByAppendingString:self.id]]; 152 | [[NSUserDefaults standardUserDefaults] synchronize]; 153 | } 154 | 155 | - (void)loadProperties 156 | { 157 | NSDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithFile:[self URLEncodedFilenameFromString:kPropertiesKey]]; 158 | 159 | if (dict) { 160 | properties = dict.mutableCopy; 161 | 162 | NSValue *savedLifeSpan = [properties objectForKey:@"lifespan"]; 163 | if (savedLifeSpan != nil) { 164 | [self setValue:savedLifeSpan forKey:@"lifespan"]; 165 | } 166 | 167 | NSNumber *savedRuturnsExpData = [properties objectForKey:@"returnsExpiredData"]; 168 | if (savedRuturnsExpData != nil) { 169 | [self setValue:savedRuturnsExpData forKey:@"returnsExpiredData"]; 170 | } 171 | 172 | NSNumber *savedEncryptionEnabled = [properties objectForKey:@"encryptionEnabled"]; 173 | if (savedEncryptionEnabled != nil) { 174 | [self setValue:savedEncryptionEnabled forKey:@"encryptionEnabled"]; 175 | } 176 | 177 | NSNumber *savedIsPersistent = [properties objectForKey:@"persistent"]; 178 | if (savedIsPersistent != nil) { 179 | [self setValue:savedIsPersistent forKey:@"persistent"]; 180 | } 181 | 182 | } else { 183 | properties = [NSMutableDictionary dictionaryWithCapacity:1]; 184 | } 185 | } 186 | 187 | - (void)addKVO 188 | { 189 | [self addObserver:self 190 | forKeyPath:@"lifespan" 191 | options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 192 | context:nil]; 193 | [self addObserver:self 194 | forKeyPath:@"returnsExpiredData" 195 | options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 196 | context:nil]; 197 | [self addObserver:self 198 | forKeyPath:@"encryptionEnabled" 199 | options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 200 | context:nil]; 201 | [self addObserver:self 202 | forKeyPath:@"persistent" 203 | options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 204 | context:nil]; 205 | } 206 | 207 | - (void)removeKVO 208 | { 209 | [self removeObserver:self forKeyPath:@"lifespan"]; 210 | [self removeObserver:self forKeyPath:@"returnsExpiredData"]; 211 | [self removeObserver:self forKeyPath:@"encryptionEnabled"]; 212 | [self removeObserver:self forKeyPath:@"persistent"]; 213 | } 214 | 215 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context 216 | { 217 | [properties setValue:change[NSKeyValueChangeNewKey] forKey:keyPath]; 218 | [self setCodableObject:properties forKey:kPropertiesKey]; 219 | } 220 | 221 | - (void)setPersistsCacheAcrossVersions:(BOOL)persistsCacheAcrossVersions 222 | { 223 | [[NSUserDefaults standardUserDefaults] setObject:@(persistsCacheAcrossVersions) forKey:[@"NOCACHE_PERSISTS_VER_" stringByAppendingString:self.id]]; 224 | [[NSUserDefaults standardUserDefaults] synchronize]; 225 | 226 | _persistsCacheAcrossVersions = persistsCacheAcrossVersions; 227 | } 228 | 229 | #pragma mark - Expiration methods 230 | 231 | - (void)loadCreationDates 232 | { 233 | NSDictionary *dict = [self dictForKey:kCreationDatesKey]; 234 | if (dict) { 235 | self.creationDates = dict.mutableCopy; 236 | } else { 237 | self.creationDates = [NSMutableDictionary dictionaryWithCapacity:1]; 238 | } 239 | } 240 | 241 | - (BOOL)objectForKeyIsValid:(NSString *)key 242 | { 243 | BOOL objectExists = NO; 244 | if ( [self.memoryCache objectForKey:key] != nil ) { 245 | objectExists = YES; 246 | } else if ( [self dataForKey:key useMemoryCache:NO] != nil ) { 247 | objectExists = YES; 248 | } 249 | 250 | if ( objectExists == NO ) { 251 | return NO; 252 | } 253 | 254 | return [self internalObjectForKeyIsValid:key]; 255 | } 256 | 257 | - (BOOL)internalObjectForKeyIsValid:(NSString *)key //For internal use. Doesn't check if object actually exists (this is done in the calling method). 258 | { 259 | if (self.lifespan == 0 ) { 260 | return YES; 261 | } 262 | 263 | NSTimeInterval timeStamp = [self lastUpdateTimeForKey:key]; 264 | 265 | if ([[NSDate date] timeIntervalSince1970] < timeStamp + self.lifespan) { 266 | return YES; 267 | } 268 | 269 | return NO; 270 | } 271 | 272 | - (NSTimeInterval)lastUpdateTimeForKey:(NSString *)key 273 | { 274 | NSDate *date = [self.creationDates objectForKey:key]; 275 | 276 | if (date) { 277 | return date.timeIntervalSince1970; 278 | } 279 | 280 | return CashierDateNotFound; 281 | } 282 | 283 | - (void)refreshValidationOnObjectForKey:(NSString *)key 284 | { 285 | @synchronized(self) { 286 | 287 | [self.creationDates setObject:[NSDate date] forKey:key]; 288 | if (self.persistent) { 289 | [self setObject:self.creationDates forKey:kCreationDatesKey]; 290 | } 291 | 292 | } 293 | } 294 | 295 | #pragma mark - Tools - 296 | 297 | - (NSString *)URLEncodedFilenameFromString:(NSString *)string 298 | { 299 | NSString *directory = [self encodedStringForUseInPath:self.id]; 300 | NSString *filename = [[self encodedStringForUseInPath:string] stringFromMD5]; 301 | 302 | return [[self.storagePath stringByAppendingPathComponent:directory] stringByAppendingPathComponent:filename]; 303 | } 304 | 305 | - (NSString *)encodedStringForUseInPath:(NSString *)string { 306 | return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]; 307 | } 308 | 309 | #pragma mark - Getters and Setters - 310 | 311 | - (void)setObject:(id)object forKey:(NSString *)key 312 | { 313 | if ( object == nil ) { 314 | [self deleteObjectForKey:key]; 315 | return; 316 | } 317 | 318 | if (self.persistent) 319 | { 320 | #if TARGET_OS_IPHONE 321 | if ([object isKindOfClass:[UIImage class]]) 322 | { 323 | @throw [NSException exceptionWithName:@"Cashier error!!" reason:@"You are using - (void)setObject:ForKey: with a UIImage. Please use - (void)setImage:ForKey:. Use -(void)imageForKey: to retrieve it." userInfo:nil]; 324 | } 325 | else 326 | #endif 327 | if ([object respondsToSelector:@selector(writeToFile:options:error:)]) 328 | { 329 | [self setWritableObject:object forKey:key useMemoryCache:YES]; 330 | } 331 | else if ([object conformsToProtocol:NSProtocolFromString(@"NSCoding")]) 332 | { 333 | [self setCodableObject:object forKey:key]; 334 | } 335 | else 336 | { 337 | [self.memoryCache setObject:object forKey:key]; 338 | NSLog(@"Cashier Error! Object: %@ is not supported for persistent caching! Object only cached in memory.", object); 339 | } 340 | } else { 341 | [self.memoryCache setObject:object forKey:key]; 342 | [self refreshValidationOnObjectForKey:key]; 343 | } 344 | } 345 | #if TARGET_OS_IPHONE 346 | - (void)setImage:(UIImage *)image forKey:(NSString *)key 347 | { 348 | [self.memoryCache setObject:image forKey:key]; 349 | 350 | if (self.persistent) { 351 | [self setObject:@(image.scale) forKey:[key stringByAppendingString:@"_scl_"]]; 352 | [self setObject:@(image.imageOrientation) forKey:[key stringByAppendingString:@"_orientation_"]]; 353 | [self setWritableObject:UIImagePNGRepresentation(image) forKey:key useMemoryCache:NO]; 354 | } 355 | } 356 | #endif 357 | - (void)setData:(NSData *)data forKey:(NSString *)key 358 | { 359 | [self setData:data forKey:key useMemoryCache:YES]; 360 | } 361 | 362 | - (void)setData:(NSData *)data forKey:(NSString *)key useMemoryCache:(BOOL)useMemoryCache 363 | { 364 | if (useMemoryCache) { 365 | [self.memoryCache setObject:data forKey:key]; 366 | } 367 | 368 | [self setWritableObject:data forKey:key useMemoryCache:NO]; 369 | } 370 | 371 | - (void)setWritableObject:(id)object forKey:(NSString *)key useMemoryCache:(BOOL)useMemoryCache 372 | { 373 | if (useMemoryCache) { 374 | [self.memoryCache setObject:object forKey:key]; 375 | } 376 | 377 | NSDataWritingOptions writeOptions = 0; 378 | 379 | if (self.encryptionEnabled) { 380 | #if TARGET_OS_IPHONE 381 | writeOptions = NSDataWritingFileProtectionComplete | NSDataWritingAtomic; 382 | #else 383 | writeOptions = NSDataWritingAtomic; 384 | #endif 385 | } 386 | 387 | NSError *error; 388 | 389 | NSURL *fileURL = [NSURL fileURLWithPath:[self URLEncodedFilenameFromString:key]]; 390 | 391 | BOOL succes = [(NSData *)object writeToFile:[self URLEncodedFilenameFromString:key] 392 | options:writeOptions 393 | error:&error]; 394 | 395 | [self fileURLWillBeWritten:fileURL]; 396 | 397 | if (!succes) { 398 | NSLog(@"Failed to write object of type: %@ \n for key: %@ \n filename: %@ \n reason: %@", [object class], key, [self URLEncodedFilenameFromString:key], error.localizedDescription); 399 | } else { 400 | if (![key isEqualToString:kCreationDatesKey]) { 401 | [self refreshValidationOnObjectForKey:key]; 402 | } 403 | } 404 | } 405 | 406 | - (void)fileURLWillBeWritten:(NSURL *)url 407 | {} 408 | 409 | - (void)setCodableObject:(id )object forKey:(NSString *)key 410 | { 411 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object]; 412 | [self setData:data forKey:key useMemoryCache:NO]; 413 | [self.memoryCache setObject:object forKey:key]; 414 | } 415 | 416 | 417 | - (id)objectForKey:(NSString *)key 418 | { 419 | id object = [self.memoryCache objectForKey:key]; 420 | 421 | if (![self internalObjectForKeyIsValid:key] && !self.returnsExpiredData) { 422 | return nil; 423 | } 424 | 425 | if (object == nil && self.persistent) 426 | { 427 | @try { 428 | object = [NSKeyedUnarchiver unarchiveObjectWithFile:[self URLEncodedFilenameFromString:key]]; 429 | } 430 | @catch (NSException *exception) { 431 | object = [self dataForKey:key useMemoryCache:NO]; 432 | } 433 | 434 | if (object != nil) { 435 | [self.memoryCache setObject:object forKey:key]; 436 | } 437 | } 438 | 439 | return object; 440 | } 441 | #if TARGET_OS_IPHONE 442 | - (UIImage *)imageForKey:(NSString *)key 443 | { 444 | if (![self internalObjectForKeyIsValid:key] && !self.returnsExpiredData) { 445 | return nil; 446 | } 447 | 448 | UIImage *image = [self.memoryCache objectForKey:key]; 449 | 450 | if (image == nil && self.persistent) { 451 | 452 | image = [UIImage imageWithData:[self dataForKey:key useMemoryCache:NO]]; 453 | 454 | if (image) { 455 | NSNumber *savedScale = [self objectForKey:[key stringByAppendingString:@"_scl_"]]; 456 | NSNumber *savedOrientation = [self objectForKey:[key stringByAppendingString:@"_orientation_"]]; 457 | 458 | if (savedScale) { 459 | image = [UIImage imageWithCGImage:image.CGImage scale:savedScale.floatValue orientation:savedOrientation.intValue]; 460 | } 461 | [self.memoryCache setObject:image forKey:key]; 462 | } 463 | } 464 | 465 | return image; 466 | } 467 | #endif 468 | 469 | - (NSData *)dataForKey:(NSString *)key 470 | { 471 | return [self dataForKey:key useMemoryCache:YES]; 472 | } 473 | 474 | - (NSData *)dataForKey:(NSString *)key useMemoryCache:(BOOL)useMemoryCache 475 | { 476 | if (![self internalObjectForKeyIsValid:key] && !self.returnsExpiredData) { 477 | return nil; 478 | } 479 | 480 | id object = nil; 481 | 482 | if (useMemoryCache) { 483 | object = [self.memoryCache objectForKey:key]; 484 | } 485 | 486 | if (object == nil && self.persistent) { 487 | object = [NSData dataWithContentsOfFile:[self URLEncodedFilenameFromString:key]]; 488 | 489 | if (object && useMemoryCache) { 490 | [self.memoryCache setObject:object forKey:key]; 491 | } 492 | } 493 | return object; 494 | } 495 | 496 | - (NSDictionary *)dictForKey:(NSString *)key 497 | { 498 | return [self objectForKey:key]; 499 | } 500 | 501 | - (NSArray *)arrayForKey:(NSString *)key 502 | { 503 | return [self objectForKey:key]; 504 | } 505 | 506 | - (NSDictionary *)dictionaryForKey:(NSString *)key; 507 | { 508 | return [self objectForKey:key]; 509 | } 510 | 511 | 512 | - (NSString *)stringForKey:(NSString *)key 513 | { 514 | NSString *string = [self.memoryCache objectForKey:key]; 515 | 516 | if (string == nil && self.persistent) { 517 | 518 | NSData *data = [self dataForKey:key useMemoryCache:NO]; 519 | 520 | if (data) { 521 | 522 | string = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; 523 | if (string) { 524 | [self.memoryCache setObject:string forKey:key]; 525 | } 526 | } 527 | } 528 | 529 | return string; 530 | } 531 | 532 | - (void)deleteObjectForKey:(NSString *)key 533 | { 534 | [self.memoryCache removeObjectForKey:key]; 535 | [[NSFileManager defaultManager] removeItemAtPath:[self URLEncodedFilenameFromString:key] error:nil]; 536 | } 537 | 538 | 539 | - (void)clearAllData 540 | { 541 | [self.memoryCache removeAllObjects]; 542 | 543 | NSString *dir = [self.storagePath stringByAppendingPathComponent:[self encodedStringForUseInPath:self.id]]; 544 | 545 | NSFileManager* fm = [NSFileManager defaultManager]; 546 | NSDirectoryEnumerator* en = [fm enumeratorAtPath:dir]; 547 | NSError* err = nil; 548 | BOOL res; 549 | 550 | NSString* file; 551 | while (file = [en nextObject]) { 552 | res = [fm removeItemAtPath:[dir stringByAppendingPathComponent:file] error:&err]; 553 | if (!res && err) { 554 | NSLog(@"Error deleting file %@", err); 555 | } 556 | } 557 | } 558 | 559 | #pragma mark - Version stuff 560 | 561 | static NSInteger maxValues = 3; 562 | 563 | + (BOOL)isVersion:(NSString *)versionA greaterThanVersion:(NSString *)versionB 564 | { 565 | if ( versionA == nil || versionB == nil ) { 566 | return NO; 567 | } 568 | 569 | NSArray *versionAArray = [versionA componentsSeparatedByString:@"."]; 570 | maxValues = versionAArray.count; 571 | versionAArray = [self normalizedValuesFromArray:versionAArray]; 572 | 573 | NSArray *versionBArray = [versionB componentsSeparatedByString:@"."]; 574 | versionBArray = [self normalizedValuesFromArray:versionBArray]; 575 | 576 | for (NSInteger i=0; i[[versionBArray objectAtIndex:i] integerValue]) { 578 | return TRUE; 579 | } else if ([[versionAArray objectAtIndex:i] integerValue]<[[versionBArray objectAtIndex:i] integerValue]) { 580 | return FALSE; 581 | } 582 | } 583 | return FALSE; 584 | } 585 | 586 | + (NSArray *)normalizedValuesFromArray:(NSArray *)array{ 587 | NSMutableArray *mutableArray = [array mutableCopy]; 588 | if([mutableArray count]