├── 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 |
6 |
7 |
8 | Cashier is a caching framework that makes it easy to work with persistent data.
9 |
10 | [](https://circleci.com/gh/nodes-ios/Cashier)
11 | [](https://codecov.io/github/nodes-ios/Cashier)
12 | [](https://cocoapods.org/pods/Cashier)
13 | [](https://github.com/Carthage/Carthage)
14 | 
15 | [](https://github.com/nodes-ios/Codemine/blob/master/LICENSE)
16 | [](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]