├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── main.yml ├── .gitignore ├── .jazzy.yaml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Assets └── CodablePersistLogo.png ├── CodablePersist.podspec ├── CodablePersist.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── CodablePersist-iOS.xcscheme │ ├── CodablePersist-macOS.xcscheme │ ├── CodablePersist-tvOS.xcscheme │ └── CodablePersist-watchOS.xcscheme ├── Configs ├── CodablePersist.plist └── CodablePersistTests.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Core │ ├── CacheStorable.swift │ ├── Identifiable.swift │ ├── ObjectDescriptor.swift │ ├── StorageReadable.swift │ ├── StorageRemovable.swift │ └── StorageWritable.swift ├── Helpers │ ├── SorageDateDescriptor.swift │ └── StorageError.swift └── Storage │ ├── DiskStorage.swift │ ├── MemoryStorage.swift │ └── UserDefaultsStorage.swift ├── Tests ├── DiskStorageTest.swift ├── MemoryStorageTest.swift ├── Mock │ └── PostMock.swift └── UserDefaultsStorageTest.swift ├── docs ├── Classes.html ├── Classes │ ├── DiskStorage.html │ ├── MemoryStorage.html │ └── UserDefaultsStorage.html ├── Enums.html ├── Enums │ └── SorageDateDescriptor.html ├── Protocols.html ├── Protocols │ ├── Identifiable.html │ ├── StorageReadable.html │ ├── StorageRemovable.html │ └── StorageWritable.html ├── Structs.html ├── badge.svg ├── css │ ├── highlight.css │ └── jazzy.css ├── docsets │ ├── CodablePersist.docset │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ ├── Documents │ │ │ ├── Classes.html │ │ │ ├── Classes │ │ │ │ ├── DiskStorage.html │ │ │ │ ├── MemoryStorage.html │ │ │ │ └── UserDefaultsStorage.html │ │ │ ├── Enums.html │ │ │ ├── Enums │ │ │ │ └── SorageDateDescriptor.html │ │ │ ├── Protocols.html │ │ │ ├── Protocols │ │ │ │ ├── Identifiable.html │ │ │ │ ├── StorageReadable.html │ │ │ │ ├── StorageRemovable.html │ │ │ │ └── StorageWritable.html │ │ │ ├── Structs.html │ │ │ ├── css │ │ │ │ ├── highlight.css │ │ │ │ └── jazzy.css │ │ │ ├── img │ │ │ │ ├── carat.png │ │ │ │ ├── dash.png │ │ │ │ └── gh.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ │ ├── jazzy.js │ │ │ │ └── jquery.min.js │ │ │ └── search.json │ │ │ └── docSet.dsidx │ └── CodablePersist.tgz ├── img │ ├── carat.png │ ├── dash.png │ └── gh.png ├── index.html ├── js │ ├── jazzy.js │ └── jquery.min.js ├── search.json └── undocumented.json └── fastlane ├── .env └── Fastfile /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## CodablePersist Environment 11 | 12 | - CodablePersist version: 13 | - macOS version: 14 | - Xcode version: 15 | - Dependency manager (Carthage, CocoaPods, SPM, Manually): 16 | 17 | ## What did you do? 18 | 19 | > ℹ Please replace this with what you did. 20 | 21 | ## What did you expect to happen? 22 | 23 | > ℹ Please replace this with what you expected to happen. 24 | 25 | ## What happened instead? 26 | 27 | > ℹ Please replace this with of what happened instead. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for CodablePersist 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Motivation 11 | > ℹ Please replace this with your motivation. For example if your feature request is related to a problem. 12 | 13 | # Solution 14 | > ℹ Please replace this with your proposed solution. 15 | 16 | # Additional context 17 | > ℹ Please replace this with any other context or screenshots about your feature request (optional). 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | - /^hotfix.*$/ 9 | - /^feature.*$/ 10 | 11 | jobs: 12 | tests: 13 | name: Unit-Tests 14 | runs-on: macOS-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: fastlane ios tests 18 | run: fastlane ios tests 19 | compatibility: 20 | name: Compatibility-Tests 21 | runs-on: macOS-latest 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: fastlane ios compatibilityTests 25 | run: fastlane ios compatibilityTests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,xcode,carthage,cocoapods,fastlane 3 | 4 | ### Carthage ### 5 | # Carthage 6 | # 7 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 8 | Carthage/Checkouts 9 | 10 | Carthage/Build 11 | 12 | ### CocoaPods ### 13 | ## CocoaPods GitIgnore Template 14 | 15 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 16 | # - Also handy if you have a large number of dependant pods 17 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 18 | Pods/ 19 | 20 | ### macOS ### 21 | *.DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Documentation 32 | #Documentation 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### Xcode ### 51 | # Xcode 52 | # 53 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 54 | 55 | ## Build generated 56 | build/ 57 | DerivedData/ 58 | 59 | ## Various settings 60 | *.pbxuser 61 | !default.pbxuser 62 | *.mode1v3 63 | !default.mode1v3 64 | *.mode2v3 65 | !default.mode2v3 66 | *.perspectivev3 67 | !default.perspectivev3 68 | xcuserdata/ 69 | 70 | ## Other 71 | *.moved-aside 72 | *.xccheckout 73 | *.xcscmblueprint 74 | 75 | ### Xcode Patch ### 76 | *.xcodeproj/* 77 | !*.xcodeproj/project.pbxproj 78 | !*.xcodeproj/xcshareddata/ 79 | !*.xcworkspace/contents.xcworkspacedata 80 | /*.gcno 81 | 82 | ### fastlane ### 83 | # fastlane - A streamlined workflow tool for Cocoa deployment 84 | # 85 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 86 | # screenshots whenever they are needed. 87 | # For more information about the recommended setup visit: 88 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 89 | 90 | # fastlane specific 91 | /fastlane/README.md 92 | fastlane/report.xml 93 | 94 | # deliver temporary files 95 | fastlane/Preview.html 96 | 97 | # snapshot generated screenshots 98 | fastlane/screenshots/**/*.png 99 | fastlane/screenshots/screenshots.html 100 | 101 | # scan temporary files 102 | fastlane/test_output 103 | 104 | # End of https://www.gitignore.io/api/macos,xcode,carthage,cocoapods,fastlane -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | # CodablePersist document generator jazzy settings 2 | 3 | copyright: Copyright 2020 Ali Hilal 4 | author: Ali A. Hilal 5 | xcodebuild_arguments: [-target, CodablePersist-iOS] 6 | clean: true 7 | output: ./Documentation 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: # some rules are only opt-in 2 | - empty_count 3 | - redundant_nil_coalescing 4 | - switch_case_on_newline 5 | - force_unwrapping 6 | - closure_spacing 7 | - implicitly_unwrapped_optional 8 | - sorted_imports 9 | - valid_docs 10 | - nslocalizedstring_require_bundle 11 | 12 | included: 13 | - Sources 14 | 15 | excluded: # paths to ignore during linting. Takes precedence over `included`. 16 | - Tests 17 | - Example 18 | 19 | disabled_rules: # rule identifiers to exclude from running 20 | - trailing_whitespace 21 | - type_name 22 | - identifier_name 23 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Assets/CodablePersistLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/Assets/CodablePersistLogo.png -------------------------------------------------------------------------------- /CodablePersist.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "CodablePersist" 3 | s.version = "0.1" 4 | s.summary = "Store and Cache Anything Codable" 5 | s.homepage = "https://github.com/engali94/CodablePersist.git" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Ali A, Hilal" => "ali94_200970@yahoo.com" } 8 | s.source = { :git => "https://github.com/engali94/CodablePersist.git", :tag => s.version.to_s } 9 | s.swift_version = "5.1" 10 | s.ios.deployment_target = "8.0" 11 | s.tvos.deployment_target = "9.0" 12 | s.watchos.deployment_target = "2.0" 13 | s.osx.deployment_target = "10.10" 14 | s.source_files = "Sources/**/*" 15 | s.frameworks = "Foundation" 16 | end 17 | -------------------------------------------------------------------------------- /CodablePersist.xcodeproj/xcshareddata/xcschemes/CodablePersist-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /CodablePersist.xcodeproj/xcshareddata/xcschemes/CodablePersist-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /CodablePersist.xcodeproj/xcshareddata/xcschemes/CodablePersist-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /CodablePersist.xcodeproj/xcshareddata/xcschemes/CodablePersist-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 48 | 49 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 73 | 74 | 75 | 76 | 78 | 79 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Configs/CodablePersist.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | CodablePersist 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 0.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 AliHilal. All rights reserved. 27 | NSPrincipalClass 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Configs/CodablePersistTests.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 | 0.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Ali Hilal 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 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CodablePersist", 7 | platforms: [ 8 | .iOS(.v8), 9 | .tvOS(.v9), 10 | .watchOS(.v2), 11 | .macOS(.v10_10) 12 | ], 13 | products: [ 14 | .library( 15 | name: "CodablePersist", 16 | targets: ["CodablePersist"] 17 | ), 18 | ], 19 | dependencies: [], 20 | targets: [ 21 | .target( 22 | name: "CodablePersist", 23 | dependencies: [], 24 | path: "Sources" 25 | ), 26 | .testTarget( 27 | name: "CodablePersistTests", 28 | dependencies: ["CodablePersist"], 29 | path: "Tests" 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | CodablePersist Logo 5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 | 13 | 14 | Swift 5.0 15 | 16 | 17 | 18 | 19 | 20 | Version 21 | 22 | 23 | 24 | 25 | 26 | Platform 27 | 28 | 29 | 30 | 31 | 32 | Carthage Compatible 33 | 34 | 35 | 36 | 37 | 38 | SPM 39 | 40 | 41 | 42 |

43 | 44 | 45 | 46 | # CodablePersist 47 | 48 | 49 | 50 |

51 | 52 | Store your awsome `Codable` objects and retrieve them with ease. `CodablePersist` gives you a convenient way to store your objects in a disk, UserDefaluts, and memory and manage them easily. 53 | 54 |

55 | 56 | 57 | 58 | ## Features 59 | 60 | 61 | 62 | - ✅ Easily save and retrieve any `Codable` type. 63 | - 📆 Set expiration date to the objects. 64 | - 🔍Time-based storage filtring. 65 | - 🗃Retrieve, delete, save objects using subscript 66 | 67 | ## Example 68 | 69 | 70 | 71 | To quickly show you how `CodablePersist` can be useful, consider the following use case: 72 | 73 | ```swift 74 | class PostFetcher { 75 | typealias Handler = (Result) -> Void 76 | private let cache = DiskStorage(storeName: "postStorage") 77 | 78 | func fetchPost(withID id: Post.ID, then handler: @escaping Handler) { 79 | // check if the post is cached or not 80 | if let cached = cache[id] { return handler(.success(cached)) } 81 | // if not cached fetch it from the backend 82 | performFetching { [weak self] result in 83 | // load post and cache it 84 | let post = try? result.get() post.map { self?.cache[id] = $0 } 85 | //then return the result 86 | handler(result) 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | ## Installation 93 | 94 | 95 | 96 | ### CocoaPods 97 | 98 | 99 | 100 | CodablePersist is available through [CocoaPods](http://cocoapods.org). To install 101 | 102 | it, simply add the following line to your Podfile: 103 | 104 | 105 | 106 | ```bash 107 | 108 | pod 'CodablePersist' 109 | 110 | ``` 111 | 112 | 113 | 114 | ### Carthage 115 | 116 | 117 | 118 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. 119 | 120 | 121 | 122 | To integrate CodablePersist into your Xcode project using Carthage, specify it in your `Cartfile`: 123 | 124 | 125 | 126 | ```ogdl 127 | 128 | github "engali94/CodablePersist" 129 | 130 | ``` 131 | 132 | 133 | 134 | Run `carthage update` to build the framework and drag the built `CodablePersist.framework` into your Xcode project. 135 | 136 | 137 | 138 | On your application targets’ “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase” and add the Framework path as mentioned in [Carthage Getting started Step 4, 5 and 6](https://github.com/Carthage/Carthage/blob/master/README.md#if-youre-building-for-ios-tvos-or-watchos) 139 | 140 | 141 | 142 | ### Swift Package Manager 143 | 144 | 145 | 146 | To integrate using Apple's [Swift Package Manager](https://swift.org/package-manager/), add the following as a dependency to your `Package.swift`: 147 | 148 | 149 | 150 | ```swift 151 | 152 | dependencies: [ 153 | 154 | .package(url: "https://github.com/engali94/CodablePersist.git", from: "0.1") 155 | 156 | ] 157 | 158 | ``` 159 | Next, add `CodablePersist` to your targets as follows: 160 | ```swift 161 | .target( 162 | name: "YOUR_TARGET_NAME", 163 | dependencies: [ 164 | "CodablePersist", 165 | ] 166 | ), 167 | ``` 168 | Then run `swift package update` to install the package. 169 | 170 | 171 | Alternatively navigate to your Xcode project, select `Swift Packages` and click the `+` icon to search for `CodablePersist`. 172 | 173 | 174 | 175 | ### Manually 176 | 177 | 178 | 179 | If you prefer not to use any of the aforementioned dependency managers, you can integrate CodablePersist into your project manually. Simply drag the `Sources` Folder into your Xcode project. 180 | 181 | 182 | 183 | ## Usage 184 | 185 | ### 1. Prepare your data 186 | Make sure your type conforms to `Identifiable` protocol, and assign a unique `idKey` property like follows: 187 | ``` swift 188 | struct Post: Codable, Identifiable { 189 | // Identifiable conformance 190 | static var idKey = \Post.id 191 | 192 | var title: String 193 | var id: String // should be unique per post 194 | } 195 | ``` 196 | 197 | 198 | ### 2. Initialize Storage 199 | 200 | - `DiskStorage`: We can init a Disk Storage by passing a `storeName` and `expiryDate` 201 | ``` swift 202 | let storage = try? DiskStorage(storeName: storageName, expiryDate: .minutes(interval: 10)) 203 | ``` 204 | 205 | - `UserDefalutsStorage`: Also we can init UserDefaults Storage by passing a `storeName` and `expiryDate` 206 | ``` swift 207 | let storage = UserDefaultsStorage(storeName: storageName, expiryDate: .minutes(interval: 10))! 208 | ``` 209 | 210 | - `MemoryStorage`: This should be used with precaution, if any memory load happens the system will delete some or all objects to free memory. **Consider using `DiskStorage` or `UserDefalutsStorage` for long term persistence.** 211 | we can init `` 212 | ```swift 213 | let storage = MemoryStorage(expiryDate: .minutes(interval: 10)) 214 | ``` 215 | ### 3. Storage!! 216 | Now you are good to go... You can persist, retrieve and delete your `Codable` objects 😎 217 | #### Saving 218 | ```swift 219 | let singlePost = Post(title: "I'm persistable", id: 1) 220 | let posts: [Post] = [Post(title: "I'm persistable2", id: 2), Post(title: "I'm persistable3", id: 3)] 221 | 222 | //Save a single object 223 | try? storage.save(singlePost) 224 | 225 | // Save single object by subscript 226 | storage[1] = singlePost 227 | 228 | //Save mutliple objects 229 | try? storage.save(posts) 230 | 231 | 232 | ``` 233 | #### Retrieval 234 | ```swift 235 | // fetch single object 236 | let post1 = try? storage.fetchObject(for: 1) 237 | 238 | // fetch by subscript 239 | let post2 = storage[2] 240 | 241 | // fetch multiple objects 242 | let multiPosts = try? storage.fetchObjects(for: [3,2]) 243 | 244 | // fetch all objects in the store sorted by date ascendingly 245 | let allObjets = try? storage.fetchAllObjects(descending: false) 246 | 247 | // fetch only objects saved in las ten minutes 248 | // ------(10)++++++(now) -> will only returns ++++++ 249 | let obejetsAfterTenMin = try? storage.fetchObjectsStored(inLast: .minutes(interval: 10) 250 | 251 | // fetch object stored before the last 10 minutes 252 | // ++++++(10)------(now) -> will only returns ++++++ 253 | let obejetsBeforeTemMin = try? storage.fetchObjectsStored(before: .minutes(interval: 10)) 254 | 255 | // check if an object exists in the storage 256 | storage.contains(2) 257 | 258 | // check the number of objects in the storage 259 | storage.objectsCount 260 | 261 | ``` 262 | #### Deletion 263 | ```swift 264 | // delete a single object 265 | try? storage.deleteObject(forKey: singlePost.id) 266 | 267 | // delete a single object by subscript 268 | storage[singlePost.id] = nil 269 | 270 | // delete multiple posts 271 | try? storage.deleteObjects(forKeys: [2,3]) 272 | 273 | // delete all objects 274 | try? storage.deleteAll() 275 | 276 | // Delete expired objects 277 | storage.deleteExpired() 278 | ``` 279 | ## Requirements 280 | 281 | - iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ 282 | - Xcode 10.0+ 283 | - Swift 4.2+ 284 | 285 | ## Contributing 286 | 287 | Contributions are warmly welcomed 🙌 288 | 289 | 290 | 291 | ## License 292 | 293 | CodablePesist is released under the MIT license. See [LICENSE](https://github.com/engali94/CodablePersist/blob/master/LICENSE) for more information. 294 | -------------------------------------------------------------------------------- /Sources/Core/CacheStorable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /// A protocol that conform to `StorageReadable`, `StorageWritable`and `StorageRemovable` 25 | public protocol CacheStorable: StorageReadable, StorageWritable, StorageRemovable { } 26 | -------------------------------------------------------------------------------- /Sources/Core/Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /// A protocol conformed by objects that can be uniuqely identifed. 25 | public protocol Identifiable { 26 | 27 | associatedtype ID: Hashable & Codable 28 | 29 | /// a `KeyPath` to the uniuqe id. 30 | static var idKey: WritableKeyPath { get } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Core/ObjectDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// Object Descriptor 27 | /// 28 | /// Describe an object that need to be stored and retrieved. 29 | /// - seealso: `MemoryStorage.Entry` 30 | public struct ObjectDescriptor { 31 | 32 | /// The original object needs to be stored or fetched. 33 | let object: T 34 | 35 | /// Adds expiry date to the object 36 | let expiryDate: Date 37 | 38 | /// Adds creation date to the object. 39 | let creationDate: Date 40 | 41 | /// Object's uniuqe ID. 42 | let key: T.ID 43 | 44 | /// Checks wheather an object has expired or not. 45 | var isExpired: Bool { 46 | return Date() > expiryDate 47 | } 48 | 49 | init(object: T, expiryDate: Date, creationDate: Date ) { 50 | self.object = object 51 | self.creationDate = creationDate 52 | self.key = object[keyPath: T.idKey] 53 | self.expiryDate = expiryDate 54 | } 55 | } 56 | 57 | // MARK: Codable Conformation 58 | extension ObjectDescriptor: Codable { } 59 | -------------------------------------------------------------------------------- /Sources/Core/StorageReadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /// A protocol conformed by objects that can be read from the storage. 25 | public protocol StorageReadable { 26 | associatedtype T: Identifiable 27 | 28 | /// Retrives single object from the storage. 29 | /// 30 | /// - Parameters: 31 | /// - key: Key that uniqly identifies that object. 32 | /// - Returns: The object fetched from the specified key. 33 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 34 | func fetchObject(for key: T.ID) throws -> T? 35 | 36 | /// Retrives multiple objects from the storage. 37 | /// 38 | /// - Parameters: 39 | /// - keys: An array of keys that uniqly identifies the stored objects. 40 | /// - Returns: Array of objects fetched from the specified keys. 41 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 42 | func fetchObjects(for keys: [T.ID]) throws -> [T]? 43 | 44 | /// Retrives all valid (not expired) objects from the storage. 45 | /// 46 | /// - Parameters: 47 | /// - descending: The order of the fetched results, deafults to `true`. 48 | /// - Returns: Array of all valid (not expired) objects fetched from the storage. 49 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 50 | func fetchAllObjects(descending: Bool) throws -> [T]? 51 | 52 | /// Retrives a chunk of objects stored _after_ a specific date. i.e in the last ten minutes. 53 | /// 54 | ///- Parameters: 55 | /// - interval: An interval of time. 56 | /// - Returns: Array of objects fetched from the storage stored after the specified period. 57 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 58 | func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] 59 | 60 | /// Retrives a chunk of objects stored _before_ a specific date. i.e before the last ten minutes. 61 | /// 62 | /// - Parameters: 63 | /// - interval: An interval of time. 64 | /// - Returns: Array of objects fetched from the storage stored after the specified period. 65 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 66 | func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] 67 | 68 | /// Checks if the storage contains an object with the specifed key. 69 | /// 70 | /// - Parameters: 71 | /// - key: object's uniuqe identifer. 72 | /// - Returns: Boolean value. 73 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied). 74 | func contains(_ key: T.ID) -> Bool 75 | 76 | /// Checks object associated with that key has expired or not. 77 | /// 78 | /// - Parameters: 79 | /// - key: object's uniuqe identifer. 80 | /// - Returns: Boolean value. 81 | func isExpired(for key: T.ID) -> Bool 82 | 83 | /// Gives the count of all the objects in that store. 84 | /// 85 | /// - Returns: Objects count in the store. 86 | var objectsCount: Int { get } 87 | 88 | /// Retrives a single value from th store throw subscript ([]) 89 | /// 90 | /// - Returns: optinal single object. 91 | subscript(key: T.ID) -> T? { get set } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Core/StorageRemovable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /// A protocol conformed by objects that can be removed from the storage. 25 | /// 26 | /// **Assumptions**: 27 | /// - `CodablePersist` creates objects with the associated keys. 28 | /// - The objects are stored in unique paths 29 | /// - Each kay represents only one object on that path. 30 | /// - Each object can be removed if its unique identifer is known or its life cycle ended (i.e. expired). 31 | public protocol StorageRemovable { 32 | 33 | associatedtype T: Identifiable 34 | 35 | /// Delete an object related to the specified key. 36 | /// 37 | /// - Parameters: 38 | /// - key: The unique key for that object. 39 | /// - Throws: Error encountered during the deleting process (e.g. Missing object). 40 | func deleteObject(forKey key: T.ID) throws 41 | 42 | /// Delete objects related to the specified keys. 43 | /// 44 | /// - Parameters: 45 | /// - keys: An array of unique keys for objects wanted to be deleted. 46 | /// - Throws: Error encountered during the deleting process (e.g. Missing object(s)). 47 | func deleteObjects(forKeys keys: [T.ID]) throws 48 | 49 | /// Delete all objects stored in the storage. 50 | /// 51 | /// - Throws: Error encountered during the deleting process (e.g. Missing object(s)). 52 | func deleteAll() throws 53 | 54 | /// Delete all objects that has expired. 55 | func deleteExpired() 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Core/StorageWritable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /// A protocol conformed by objects that can be stored in the storage. 25 | /// 26 | /// **Assumptions**: 27 | /// - `CodablePersist` creates objects with the associated keys. 28 | /// - The objects are stored in unique paths 29 | /// - Each key represents only one object on that path. 30 | public protocol StorageWritable { 31 | associatedtype T: Identifiable 32 | 33 | /// Saves an objectto the storage, each type conforming to `Identifiable`, will have a uniuqe ID 34 | /// this will be extracted from that object to to uniuqely identify it. If `DiskStorage` is usedit will 35 | /// use that key to generate a uniqe path to the object in the storage, also in the other storages will 36 | /// be used as amunique identifier 37 | /// 38 | /// - Parameters: 39 | /// - object: The object wanted to be stored.. 40 | /// - Throws: Error encountered during the saving process (e.g. Path incorrect, or Encoding falied). 41 | func save(_ object: T) throws 42 | 43 | /// Saves multiple object in the storage one time. 44 | /// 45 | /// - Parameters: 46 | /// - objects: An array of objects wanted to be stored.. 47 | /// - Throws: Error encountered during the saving process (e.g. Path incorrect, or Encoding falied). 48 | func save(_ objects: [T]) throws 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Helpers/SorageDateDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// Sorage Date Descriptor 27 | /// 28 | /// Describes object's expiry date as well as a time snapshot used in fetching 29 | /// objects based on a specific time interval 30 | public enum SorageDateDescriptor { 31 | 32 | /// Fetch the object stored in the last `interval` seconds, 33 | /// or set the object to be expired after `interval` seconds. 34 | case seconds(interval: Double) 35 | 36 | /// Fetch the object stored in the last `interval` minutes, 37 | /// or set the object to be expired after `interval` minutes. 38 | case minutes(interval: Double) 39 | 40 | /// Fetch the object stored in the last `interval` houres, 41 | /// or set the object to be expired after `interval` houres. 42 | case houres(interval: Double) 43 | 44 | /// Fetch the object stored in the last `interval` days, 45 | /// or set the object to be expired after `interval` days. 46 | case days(interval: Double) 47 | 48 | /// Fetch the object stored in the last `interval` months, 49 | /// or set the object to be expired after `interval` months. 50 | case months(interval: Double) 51 | 52 | /// **Warning**: Don't use this case with object retival. Use it only to set object's expiry. 53 | case never 54 | 55 | } 56 | 57 | // MARK: - Helpers 58 | 59 | extension SorageDateDescriptor { 60 | 61 | /// Converts time interval to `Date` object. 62 | /// The date will be in the past. 63 | var toDate: Date { 64 | switch self { 65 | case .seconds(let interval): 66 | return Date(timeIntervalSinceNow: -interval) 67 | 68 | case .minutes(let interval): 69 | return Date(timeIntervalSinceNow: -60 * interval) 70 | 71 | case .houres(let interval): 72 | return Date(timeIntervalSinceNow: -60 * 60 * interval) 73 | 74 | case .days(let interval): 75 | return Date(timeIntervalSinceNow: -60 * 60 * 24 * interval) 76 | 77 | case .months(let interval): 78 | return Date(timeIntervalSinceNow: -60 * 60 * 24 * 30 * interval) 79 | 80 | case .never: 81 | return Date() 82 | } 83 | } 84 | 85 | /// Converts time interval to `Date` object. 86 | /// The date will be in the future, 87 | var expireDate: Date { 88 | switch self { 89 | case .seconds(let interval): 90 | return Date().addingTimeInterval(interval) 91 | 92 | case .minutes(let interval): 93 | return Date().addingTimeInterval(60 * interval) 94 | 95 | case .houres(let interval): 96 | return Date().addingTimeInterval(60 * 60 * interval) 97 | 98 | case .days(let interval): 99 | return Date().addingTimeInterval(60 * 60 * 24 * interval) 100 | 101 | case .months(let interval): 102 | return Date().addingTimeInterval(60 * 60 * 24 * 30 * interval) 103 | 104 | case .never: 105 | return Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 68) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Helpers/StorageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// Describes various storage errors. 27 | enum StorageError: Swift.Error { 28 | 29 | /// The specified path could not be found 30 | case pathNotFound 31 | 32 | /// Obectt can't be stored or encodded. 33 | case objectUnreadable 34 | 35 | /// Object can't be fetched or encodded. 36 | case objectUnwritable 37 | 38 | /// Can't encode object 39 | case objectEncodingFailed(underlying: Swift.Error) 40 | 41 | /// Can't decode object 42 | case objectDecodingFailed(underlying: Swift.Error) 43 | } 44 | 45 | extension StorageError: LocalizedError { 46 | 47 | public var errorDescription: String? { 48 | switch self { 49 | case .pathNotFound: 50 | return "The object couldn't be found at the specified path." 51 | case .objectUnwritable: 52 | return "Obectt can't be stored or encodded." 53 | case .objectUnreadable: 54 | return "Object can't be fetched or encodded." 55 | case .objectEncodingFailed(let error): 56 | return "Can't encode object due to \(error.localizedDescription)" 57 | case .objectDecodingFailed(let error): 58 | return "Can't decode object due to \(error.localizedDescription)" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/Storage/DiskStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Disk Storage utilizes `FileManager` to cache and retrive data to and from the disk 4 | /// it gives a convenient and easy to use way to cache any `Codable` and `Identifiable` object. 5 | public class DiskStorage { 6 | 7 | private let fileManager: FileManager 8 | 9 | /// Storage's name. 10 | /// 11 | /// **Warning**: This name should be unique per storage if you want to make two or more different storages. 12 | public let storeName: String 13 | 14 | /// A `URL` refers to preferted storage dorectory. 15 | public let directoryUrl: URL 16 | 17 | /// A `String `path refers to preferted storage dorectory. 18 | public let path: String 19 | 20 | /// Expiry date of the stored objects. 21 | public let expiryDate: SorageDateDescriptor 22 | 23 | /// JSON encoder to be used for encoding objects to be stored. 24 | open var encoder: JSONEncoder 25 | 26 | /// JSON decoder to be used to decode stored objects. 27 | open var decoder: JSONDecoder 28 | 29 | /// injectable `Date` object. 30 | private let dateProvider: () -> Date 31 | 32 | /// Initialize disk storage with the given name and expiry date. 33 | /// 34 | /// - Parameters: 35 | /// - storeName: Disk storage unique identifier. 36 | /// - directoryUrl: A `URL` refers to preferted storage dorectory. _if not given will use `cachesDirectory` 37 | /// as storage directory. 38 | /// - expiryDate: The expiry date of each object being stored. 39 | /// - fileManager: system's default file manager. 40 | /// - encoder: JSON encoder to be used for encoding objects to be stored. 41 | /// - decoder: JSON decoder to be used to decode stored objects. 42 | 43 | required public init( 44 | dateProvider: @escaping () -> Date = Date.init, 45 | storeName: String, 46 | directoryUrl: URL? = nil, 47 | expiryDate: SorageDateDescriptor = .never, 48 | fileManager: FileManager = FileManager.default, 49 | encoder: JSONEncoder = .init(), 50 | decoder: JSONDecoder = .init() 51 | 52 | ) throws { 53 | self.storeName = storeName 54 | self.fileManager = fileManager 55 | self.encoder = encoder 56 | self.decoder = decoder 57 | self.expiryDate = expiryDate 58 | self.dateProvider = dateProvider 59 | 60 | if let url = directoryUrl { 61 | self.directoryUrl = url 62 | } else { 63 | self.directoryUrl = try fileManager.url( 64 | for: .cachesDirectory, 65 | in: .userDomainMask, 66 | appropriateFor: nil, 67 | create: true) 68 | } 69 | 70 | let url = self.directoryUrl.appendingPathComponent(storeName, isDirectory: true) 71 | self.path = url.path 72 | try createFolder(at: url) 73 | 74 | } 75 | 76 | } 77 | // MARK: - CacheStorable 78 | 79 | extension DiskStorage: CacheStorable { 80 | 81 | public func save(_ object: T) throws { 82 | 83 | let entry = ObjectDescriptor(object: object, expiryDate: expiryDate.expireDate, creationDate: dateProvider()) 84 | let data = try encoder.encode(entry) 85 | let filePath = makePath(for: object) 86 | 87 | fileManager.createFile(atPath: filePath, contents: data, attributes: [FileAttributeKey.creationDate: Date()]) 88 | } 89 | 90 | public func save(_ objects: [T]) throws { 91 | 92 | for object in objects { 93 | try save(object) 94 | } 95 | } 96 | 97 | public func fetchObject(for key: T.ID) throws -> T? { 98 | 99 | let filePath = makePath(for: key) 100 | guard let data = fileManager.contents(atPath: filePath) else { return nil } 101 | 102 | let entry = try decoder.decode(ObjectDescriptor.self, from: data) 103 | if !entry.isExpired { 104 | return entry.object 105 | } 106 | return nil 107 | } 108 | 109 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? { 110 | 111 | return try keys.compactMap { try fetchObject(for: $0) } 112 | } 113 | 114 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] { 115 | 116 | return try listAllEntries().filter { $0.creationDate > interval.toDate }.map { $0.object } 117 | } 118 | 119 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] { 120 | 121 | return try listAllEntries().filter { $0.creationDate < interval.toDate }.map { $0.object } 122 | } 123 | 124 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? { 125 | 126 | return try listAllEntries().sorted { (en1, en2) -> Bool in 127 | if descending { 128 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending 129 | } else { 130 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending 131 | } 132 | }.map { $0.object } 133 | } 134 | 135 | public func contains(_ key: T.ID) -> Bool { 136 | do { 137 | return try fetchObject(for: key) != nil 138 | } catch { 139 | return false 140 | } 141 | 142 | } 143 | 144 | public subscript(key: T.ID) -> T? { 145 | get { 146 | return try? fetchObject(for: key) 147 | } 148 | set { 149 | guard let value = newValue else { 150 | // If nil was assigned using subscript, 151 | // then we remove any value for that key: 152 | try? deleteObject(forKey: key) 153 | return 154 | } 155 | try? save(value) 156 | } 157 | } 158 | 159 | public func isExpired(for key: T.ID) -> Bool { 160 | return !contains(key) 161 | } 162 | 163 | public var objectsCount: Int { 164 | do { 165 | let objects = try fileManager.contentsOfDirectory(atPath: path) 166 | return objects.count 167 | } catch { 168 | return 0 169 | } 170 | } 171 | 172 | public func deleteObject(forKey key: T.ID) throws { 173 | 174 | let objectPath = makePath(for: key) 175 | try fileManager.removeItem(atPath: objectPath) 176 | } 177 | 178 | public func deleteObjects(forKeys keys: [T.ID]) throws { 179 | 180 | for key in keys { 181 | try deleteObject(forKey: key) 182 | } 183 | } 184 | 185 | public func deleteAll() throws { 186 | 187 | let objects = try fileManager.contentsOfDirectory(atPath: path) 188 | 189 | for object in objects { 190 | let objectPath = pathFromFileName(object) 191 | try fileManager.removeItem(atPath: objectPath) 192 | } 193 | } 194 | 195 | public func deleteExpired() { 196 | _ = try? listAllEntries() 197 | } 198 | } 199 | 200 | // MARK: - Helpers 201 | 202 | private extension DiskStorage { 203 | 204 | /// Create folder at the specifed url 205 | /// 206 | /// - Parameters: 207 | /// - url: URL to crate the folder at. 208 | /// - Throws: FilerManager error. 209 | func createFolder(at url: URL) throws { 210 | 211 | if !fileManager.fileExists(atPath: url.path) { 212 | try fileManager.createDirectory( 213 | at: url, 214 | withIntermediateDirectories: true, 215 | attributes: nil 216 | ) 217 | } 218 | } 219 | 220 | /// List all objects in the store and filter the expired ones 221 | /// 222 | /// - Throws: JSON decoding error. 223 | /// - Returns: array of `StorableObject` 224 | @discardableResult 225 | private func listAllEntries() throws -> [ObjectDescriptor] { 226 | 227 | let files = try fileManager.contentsOfDirectory(atPath: path) 228 | return try files.compactMap { file -> ObjectDescriptor? in 229 | guard let data = fileManager.contents(atPath: pathFromFileName(file)) else { return nil } 230 | let entry = try decoder.decode(ObjectDescriptor.self, from: data) 231 | if !entry.isExpired { 232 | return entry 233 | } else { 234 | // If the object has aleady expired 235 | // delete it and return nil 236 | try deleteObject(forKey: entry.key) 237 | return nil 238 | } 239 | } 240 | } 241 | 242 | /// Extract the key from object. 243 | /// 244 | /// - Parameters: 245 | /// - object: `T` object extract they key from. 246 | /// - Returns: unique path component of the passed object. 247 | func extractKey(from object: T) -> String { 248 | return "\(storeName)-\(object[keyPath: T.idKey])" 249 | } 250 | 251 | /// Reconstructs object's name from the passed key. 252 | /// 253 | /// - Parameters: 254 | /// - id: `T.ID` key to reconstruct path from it 255 | /// - Returns: unique path componen from the passed key. 256 | func reverseKey(for id: T.ID) -> String { 257 | return "\(storeName)-\(id)" 258 | } 259 | 260 | /// Construct a path to the passed object. 261 | /// 262 | /// - Parameters: 263 | /// - object: `T` object extract they key from. 264 | /// - Returns: a complete path of the passed object. 265 | func makePath(for object: T) -> String { 266 | return "\(path)/\(extractKey(from: object))" 267 | } 268 | 269 | /// Construct a path from the passed key. 270 | /// 271 | /// - Parameters: 272 | /// - key: a `T.ID` key to reconstruct path from it 273 | /// - Returns: a complete path of the passed key. 274 | func makePath(for id: T.ID) -> String { 275 | return "\(path)/\(reverseKey(for: id))" 276 | } 277 | 278 | /// Construct a path from the passed file name. 279 | /// 280 | /// - Parameters: 281 | /// - name: a `String` file name to reconstruct path from it 282 | /// - Returns: a complete path of the passed file name. 283 | func pathFromFileName(_ name: String) -> String { 284 | return path + "/" + name 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Sources/Storage/MemoryStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// Memory Storage utilizes `NSCache` to cache and retrive data to and from memory it gives a 27 | /// convenient and easy to use way to cache any `Codable` and `Identifiable` object. 28 | /// **WARNING**: **Don't use it for heavy objects as there will be potential lose of date if any 29 | /// memory pressure happens**. 30 | /// Consider using `DiskStorage` for heavy and long term persistence. 31 | final public class MemoryStorage { 32 | 33 | private let wrapped = NSCache() 34 | private let expiryDate: SorageDateDescriptor 35 | private let keyTracker = KeyTracker() 36 | private let dateProvider: () -> Date 37 | 38 | required public init( dateProvider: @escaping () -> Date = Date.init, 39 | expiryDate: SorageDateDescriptor, 40 | maxObjectsCount: Int = 50) { 41 | 42 | self.expiryDate = expiryDate 43 | wrapped.delegate = keyTracker 44 | wrapped.countLimit = maxObjectsCount 45 | self.dateProvider = dateProvider 46 | 47 | deleteExpired() 48 | } 49 | 50 | } 51 | 52 | // MARK: - CacheStorable 53 | 54 | extension MemoryStorage: CacheStorable { 55 | 56 | public func save(_ object: T) { 57 | 58 | let entry = Entry(object: object, 59 | creationDate: dateProvider(), 60 | expirationDate: expiryDate.expireDate) 61 | 62 | let key = object[keyPath: T.idKey] 63 | wrapped.setObject(entry, forKey: WrappedKey(key)) 64 | keyTracker.keys.insert(key) 65 | } 66 | 67 | public func save(_ objects: [T]) { 68 | 69 | for object in objects { 70 | save(object) 71 | } 72 | } 73 | 74 | public func fetchObject(for key: T.ID) throws -> T? { 75 | guard let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired == false else { 76 | throw StorageError.objectUnreadable 77 | } 78 | return entry.object 79 | } 80 | 81 | //Use compact map here 82 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? { 83 | var objects: [T] = [] 84 | for key in keys { 85 | guard let object = try fetchObject(for: key) else { continue } 86 | objects.append(object) 87 | } 88 | return objects 89 | } 90 | 91 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? { 92 | 93 | let entries = listEntries() 94 | return entries.sorted { (en1, en2) -> Bool in 95 | 96 | if descending { 97 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending 98 | } else { 99 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending 100 | } 101 | }.map { $0.object } 102 | } 103 | 104 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] { 105 | let entries = listEntries() 106 | return entries.filter { $0.creationDate >= interval.toDate }.map { $0.object } 107 | } 108 | 109 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] { 110 | let entries = listEntries() 111 | return entries.filter { $0.creationDate <= interval.toDate }.map { $0.object } 112 | } 113 | 114 | private func listEntries() -> [Entry] { 115 | var entires: [Entry] = [] 116 | for key in keyTracker.keys { 117 | if let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired == false { 118 | entires.append(entry) 119 | } 120 | } 121 | return entires 122 | } 123 | 124 | public subscript(key: T.ID) -> T? { 125 | 126 | get { return try? fetchObject(for: key) } 127 | set { 128 | guard let value = newValue else { 129 | // If nil was assigned using our subscript, 130 | // then we remove any value for that key: 131 | try? deleteObject(forKey: key) 132 | return 133 | } 134 | save(value) 135 | } 136 | } 137 | 138 | public func contains(_ key: T.ID) -> Bool { 139 | do { 140 | return try fetchObject(for: key) != nil 141 | } catch { 142 | return false 143 | } 144 | } 145 | 146 | public func isExpired(for key: T.ID) -> Bool { 147 | return !contains(key) 148 | } 149 | 150 | public var objectsCount: Int { 151 | deleteExpired() 152 | return keyTracker.keys.count 153 | } 154 | 155 | public func deleteObject(forKey key: T.ID) throws { 156 | wrapped.removeObject(forKey: WrappedKey(key)) 157 | } 158 | 159 | public func deleteObjects(forKeys keys: [T.ID]) throws { 160 | for key in keys { 161 | try deleteObject(forKey: key) 162 | } 163 | } 164 | 165 | public func deleteAll() throws { 166 | wrapped.removeAllObjects() 167 | } 168 | 169 | public func deleteExpired() { 170 | let keys = keyTracker.keys 171 | for key in keys { 172 | if let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired { 173 | try? deleteObject(forKey: key) 174 | } 175 | } 176 | } 177 | } 178 | 179 | // MARK: - Key Wrapper 180 | 181 | private extension MemoryStorage { 182 | 183 | /// Key wrapper helps make `T.ID` compatblw with `NSCache` key 184 | /// Should sublcalss`NSObject` to be compatible with `NSCache` key 185 | final class WrappedKey: NSObject { 186 | 187 | /// Objects's key 188 | let key: T.ID 189 | 190 | init(_ key: T.ID) { self.key = key } 191 | 192 | override var hash: Int { return key.hashValue } 193 | 194 | override func isEqual(_ object: Any?) -> Bool { 195 | guard let value = object as? WrappedKey else { 196 | return false 197 | } 198 | 199 | return value.key == key 200 | } 201 | } 202 | } 203 | 204 | // MARK: - KeyTracker 205 | 206 | private extension MemoryStorage { 207 | 208 | /// Helper class to keep tracking of the objects in the memory 209 | final class KeyTracker: NSObject, NSCacheDelegate { 210 | 211 | /// A set of the stored keys, 212 | var keys = Set() 213 | 214 | /// `NSCache` delegate method get called each time the object is being evicted from memory. 215 | func cache(_ cache: NSCache, 216 | willEvictObject object: Any) { 217 | guard let entry = object as? Entry else { 218 | return 219 | } 220 | 221 | keys.remove(entry.key) 222 | } 223 | } 224 | } 225 | 226 | // MARK: - Entry Setup 227 | 228 | private extension MemoryStorage { 229 | 230 | /// An Entry descibing the stored objects 231 | /// - seeAlso: ObjectDescriptor 232 | final class Entry { 233 | 234 | // The original object needs to be stored or fetched. 235 | let object: T 236 | 237 | /// Adds expiry date to the object 238 | let expirationDate: Date 239 | 240 | /// Adds creation date to the object. 241 | let creationDate: Date 242 | 243 | /// Object's uniuqe ID. 244 | let key: T.ID 245 | 246 | /// Checks wheather an object has expired or not. 247 | var isExpired: Bool { 248 | return Date() > expirationDate 249 | } 250 | 251 | init(object: T, creationDate: Date, expirationDate: Date) { 252 | self.object = object 253 | self.expirationDate = expirationDate 254 | self.creationDate = creationDate 255 | self.key = object[keyPath: T.idKey] 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /Sources/Storage/UserDefaultsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import Foundation 25 | 26 | /// User Defaults Storage utilizes `UserDefaults` to cache and retrive data to and from 27 | /// UserDefaluts it gives a convenient and easy to use way to cache any `Codable` and `Identifiable` 28 | /// object. **WARNING**: **Don't use it for heavy and large number of objects** 29 | /// Consider using `DiskStorage` for heavy and long term persistence. 30 | public class UserDefaultsStorage { 31 | 32 | /// Default Store. 33 | private var defaultsStore: UserDefaults 34 | 35 | /// Object's expiry date. 36 | public let expiryDate: SorageDateDescriptor 37 | 38 | /// Uniuqe store name. 39 | public var storeName: String 40 | 41 | /// JSON encoder to be used for encoding objects to be stored. 42 | public let encoder: JSONEncoder 43 | 44 | /// JSON decoder to be used to decode stored objects. 45 | public let decoder: JSONDecoder 46 | 47 | /// Key tracker used to keep track ob uniqe keys of objects stored in the store 48 | private var keyTracker: KeyTracker 49 | 50 | /// injectable `Date` object. 51 | private let dateProvider: () -> Date 52 | 53 | /// Initialize DefalutStorage with the given name and expiry date. 54 | /// 55 | /// - Parameters: 56 | /// - storeName: Defalut storage unique identifier. 57 | /// - dateProvider: injectable date object 58 | /// - encoder: JSON encoder to be used for encoding objects to be stored. 59 | /// - decoder: SON decoder to be used to decode stored objects. 60 | /// - expiryDate: The expiry date of each object being stored. 61 | required public init?( 62 | storeName: String, 63 | dateProvider: @escaping () -> Date = Date.init, 64 | encoder: JSONEncoder = .init(), 65 | decoder: JSONDecoder = .init(), 66 | expiryDate: SorageDateDescriptor = .never) { 67 | 68 | self.storeName = storeName 69 | self.dateProvider = dateProvider 70 | self.encoder = encoder 71 | self.decoder = decoder 72 | self.expiryDate = expiryDate 73 | self.keyTracker = KeyTracker(keysStoreName: storeName + "-keys") 74 | 75 | keyTracker.listAllKeys() 76 | guard let defaultsStore = UserDefaults(suiteName: storeName) else { return nil } 77 | self.defaultsStore = defaultsStore 78 | } 79 | 80 | } 81 | 82 | // MARK: - CacheStorable 83 | 84 | extension UserDefaultsStorage: CacheStorable { 85 | 86 | public func save(_ object: T) throws { 87 | let entry = ObjectDescriptor(object: object, expiryDate: expiryDate.expireDate, creationDate: dateProvider()) 88 | let data = try encoder.encode(entry) 89 | let key = extractKey(from: object) 90 | defaultsStore.set(data, forKey: key ) 91 | keyTracker.keys.insert(key) 92 | keyTracker.updateKeysStore() 93 | } 94 | 95 | public func save(_ objects: [T]) throws { 96 | 97 | for object in objects { 98 | try save(object) 99 | } 100 | } 101 | 102 | public func fetchObject(for key: T.ID) throws -> T? { 103 | 104 | guard let data = defaultsStore.data(forKey: reverseKey(from: key)) else { return nil } 105 | let entry = try decoder.decode(ObjectDescriptor.self, from: data) 106 | 107 | if !entry.isExpired { 108 | return entry.object 109 | } 110 | return nil 111 | } 112 | 113 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? { 114 | 115 | return try keys.compactMap { try fetchObject(for: $0) } 116 | } 117 | 118 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? { 119 | 120 | return try listAllObjects().sorted { (en1, en2) -> Bool in 121 | if descending { 122 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending 123 | } else { 124 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending 125 | } 126 | }.map { $0.object } 127 | } 128 | 129 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] { 130 | 131 | return try listAllObjects().filter { $0.creationDate >= interval.toDate }.map { $0.object } 132 | } 133 | 134 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] { 135 | 136 | return try listAllObjects().filter { $0.creationDate <= interval.toDate }.map { $0.object } 137 | } 138 | 139 | public func contains(_ key: T.ID) -> Bool { 140 | do { 141 | return try fetchObject(for: key) != nil 142 | } catch { 143 | return false 144 | } 145 | } 146 | 147 | public func isExpired(for key: T.ID) -> Bool { 148 | return !contains(key) 149 | } 150 | 151 | public var objectsCount: Int { 152 | // Remove expired objects first 153 | // then calculate the objects countn from keys. 154 | deleteExpired() 155 | return keyTracker.keys.count 156 | } 157 | 158 | public subscript(key: T.ID) -> T? { 159 | get { 160 | return try? fetchObject(for: key) 161 | } 162 | set { 163 | guard let value = newValue else { 164 | // If nil was assigned using subscript, 165 | // then we remove any value for that key: 166 | try? deleteObject(forKey: key) 167 | return 168 | } 169 | try? save(value) 170 | } 171 | } 172 | 173 | public func deleteObject(forKey key: T.ID) throws { 174 | let objectKey = reverseKey(from: key) 175 | defaultsStore.removeObject(forKey: objectKey) 176 | keyTracker.keys.remove(objectKey) 177 | keyTracker.updateKeysStore() 178 | } 179 | 180 | public func deleteObjects(forKeys keys: [T.ID]) throws { 181 | for key in keys { 182 | try deleteObject(forKey: key) 183 | } 184 | } 185 | 186 | public func deleteAll() throws { 187 | 188 | for key in keyTracker.keys { 189 | defaultsStore.removeObject(forKey: key) 190 | } 191 | 192 | keyTracker.keys.removeAll() 193 | keyTracker.updateKeysStore() 194 | } 195 | 196 | public func deleteExpired() { 197 | _ = try? listAllObjects() 198 | } 199 | 200 | } 201 | 202 | // MARK: - Helpers 203 | 204 | private extension UserDefaultsStorage { 205 | 206 | /// List all objects in the store and filter the expired ones 207 | /// 208 | /// - Throws: JSON decoding error. 209 | /// - Returns: array of `StorableObject`. 210 | @discardableResult 211 | func listAllObjects() throws-> [ObjectDescriptor] { 212 | 213 | return try keyTracker.keys.compactMap { key in 214 | guard let data = defaultsStore.data(forKey: key) else { return nil } 215 | let entry = try decoder.decode(ObjectDescriptor.self, from: data) 216 | if !entry.isExpired { 217 | return entry 218 | } else { 219 | // If the object has aleady expired 220 | // delete it and return nil 221 | try deleteObject(forKey: entry.key) 222 | return nil 223 | } 224 | } 225 | } 226 | 227 | /// ectract and convert the key asscoated with the object to string. 228 | /// 229 | /// - Parameters: 230 | /// - object: object. 231 | /// - Returns: unique key represented by a string. 232 | func extractKey(from object: T) -> String { 233 | return "\(storeName)-\(object[keyPath: T.idKey])" 234 | } 235 | 236 | /// ectract and convert the key asscoated with the object to string. 237 | /// 238 | /// - Parameters: 239 | /// - id: object's unique id. 240 | /// - Returns: unique key represented by a string. 241 | func reverseKey(from id: T.ID) -> String { 242 | return "\(storeName)-\(id)" 243 | } 244 | } 245 | 246 | // MARK: - KeyTracker 247 | private extension UserDefaultsStorage { 248 | 249 | /// Helper struct to keep trackk of the keys in the store 250 | struct KeyTracker { 251 | /// store keys 252 | var keys = Set() 253 | 254 | /// A seperate store to hold the keys values. 255 | let keysStore: UserDefaults? 256 | 257 | /// Unique name for keys store 258 | let keysStoreName: String 259 | 260 | init(keysStoreName: String) { 261 | self.keysStoreName = keysStoreName 262 | self.keysStore = UserDefaults(suiteName: keysStoreName) 263 | } 264 | 265 | /// list all keys in the store 266 | mutating func listAllKeys() { 267 | let keys = keysStore?.array(forKey: keysPath) as? [String] ?? [] 268 | self.keys = Set(keys) 269 | } 270 | 271 | /// update store with the new keys after each addition or deletion happens 272 | func updateKeysStore() { 273 | let keysArray = Array(keys) 274 | keysStore?.set(keysArray, forKey: keysPath) 275 | } 276 | 277 | /// Path to store the keys in the keysStore. 278 | var keysPath: String { 279 | return keysStoreName + "-keys" 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /Tests/DiskStorageTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | 25 | import XCTest 26 | @testable import CodablePersist 27 | 28 | class DiskStorageTest: XCTestCase { 29 | 30 | var storage: DiskStorage! 31 | let storageName = "storageTest" 32 | 33 | override func setUp() { 34 | super.setUp() 35 | storage = try! initStorageWith(expiry: .minutes(interval: 3)) 36 | 37 | } 38 | 39 | override func tearDown() { 40 | 41 | try? storage.deleteAll() 42 | storage = nil 43 | super.tearDown() 44 | } 45 | 46 | func testInitStorage() { 47 | 48 | XCTAssertNoThrow(try initStorageWith(expiry: .minutes(interval: 10))) 49 | } 50 | 51 | func testSaveSingleObject() { 52 | 53 | XCTAssertNoThrow(try storage.save(Post.singlePost)) 54 | XCTAssertEqual(storage.objectsCount, 1) 55 | 56 | } 57 | 58 | func testSaveMultiObjects() { 59 | 60 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 61 | XCTAssertEqual(10, storage.objectsCount) 62 | } 63 | 64 | func testFetchSingleObject() { 65 | 66 | XCTAssertNoThrow(try storage.save(Post.singlePost)) 67 | XCTAssertNotNil(try? storage.fetchObject(for: 0) ) 68 | XCTAssertEqual(0, try? storage.fetchObject(for: 0)?.id) 69 | } 70 | 71 | func testFetchMultiObjects() { 72 | 73 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 74 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) ) 75 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title) 76 | } 77 | 78 | func testFetchAllObjects() { 79 | 80 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 81 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count) 82 | } 83 | 84 | func testObjectExpiry() { 85 | 86 | storage = try! initStorageWith(expiry: .seconds(interval: -10)) 87 | 88 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 89 | XCTAssertTrue(storage.isExpired(for: 1)) 90 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count) 91 | 92 | } 93 | 94 | func testFetchStoredInLast() { 95 | 96 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4)) 97 | 98 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 99 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 3))) 100 | XCTAssertEqual(0 , try storage.fetchObjectsStored(before: .minutes(interval: 4)).count) 101 | } 102 | 103 | func testfetchStoredBefore() { 104 | 105 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 6)) 106 | 107 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 108 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3))) 109 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count) 110 | } 111 | 112 | func testSubscript() { 113 | 114 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 115 | XCTAssertTrue(storage[1]?.id == 1 ) 116 | XCTAssertFalse(storage[3]?.title == "Swift Article1") 117 | storage[1] = nil 118 | XCTAssertNil(storage[1]) 119 | } 120 | 121 | func testDeleteSingleObject() { 122 | 123 | XCTAssertNoThrow(try storage.save(Post.singlePost)) 124 | XCTAssertEqual(1, storage.objectsCount) 125 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id)) 126 | XCTAssertNil(storage[0]) 127 | } 128 | 129 | func testDeleteMultiObjects() { 130 | 131 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 132 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3])) 133 | XCTAssertEqual(7, storage.objectsCount) 134 | XCTAssertNil(storage[3]) 135 | } 136 | 137 | func testDeleteAllObjects() { 138 | 139 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 140 | XCTAssertEqual(10, storage.objectsCount) 141 | XCTAssertNoThrow(try storage.deleteAll()) 142 | XCTAssertEqual(0, storage.objectsCount) 143 | } 144 | 145 | func testStorageContains() { 146 | 147 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 148 | XCTAssert(storage.contains(2)) 149 | XCTAssertFalse(storage.contains(12)) 150 | } 151 | 152 | func testDeleteExpired() { 153 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 6)) 154 | 155 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 156 | storage.deleteExpired() 157 | XCTAssertEqual(0, storage.objectsCount) 158 | } 159 | 160 | private func initStorageWith(injectedDate: Date) throws -> DiskStorage{ 161 | return try DiskStorage(dateProvider: { () -> Date in 162 | return injectedDate 163 | }, storeName: storageName, expiryDate: .minutes(interval: -3)) 164 | } 165 | 166 | private func initStorageWith(expiry: SorageDateDescriptor) throws -> DiskStorage { 167 | return try DiskStorage(storeName: storageName, expiryDate: expiry) 168 | } 169 | } 170 | 171 | 172 | -------------------------------------------------------------------------------- /Tests/MemoryStorageTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | import XCTest 25 | @testable import CodablePersist 26 | 27 | class MemoryStorageTest: XCTestCase { 28 | 29 | var storage: MemoryStorage! 30 | 31 | override func setUp() { 32 | super.setUp() 33 | storage = MemoryStorage(expiryDate: .minutes(interval: 3)) 34 | } 35 | 36 | override func tearDown() { 37 | 38 | try? storage.deleteAll() 39 | storage = nil 40 | super.tearDown() 41 | } 42 | 43 | func testInitStorage() { 44 | storage = MemoryStorage(expiryDate: .minutes(interval: 3)) 45 | XCTAssertNotNil(storage) 46 | } 47 | 48 | 49 | func testSaveSingleObject() { 50 | storage.save(Post.singlePost) 51 | XCTAssertEqual(storage.objectsCount, 1) 52 | XCTAssertTrue(storage.contains(0)) 53 | } 54 | 55 | func testSaveMultiObjects() { 56 | storage.save(Post.dummyPosts()) 57 | XCTAssertEqual(10, storage.objectsCount) 58 | XCTAssertTrue(storage.contains(4)) 59 | XCTAssertEqual([1,2,3],[ storage[1]?.id, storage[2]?.id, storage[3]?.id]) 60 | } 61 | 62 | func testFetchSingleObject() { 63 | storage.save(Post.singlePost) 64 | XCTAssertNotNil(try storage.fetchObject(for: 0) ) 65 | XCTAssertEqual(0, try storage.fetchObject(for: 0)?.id) 66 | XCTAssertEqual("Single post test", try storage.fetchObject(for: 0)?.title) 67 | } 68 | 69 | 70 | func testFetchMultiObjects() { 71 | 72 | storage.save(Post.dummyPosts()) 73 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) ) 74 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title) 75 | } 76 | 77 | func testFetchAllObjects() { 78 | 79 | storage.save(Post.dummyPosts()) 80 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count) 81 | XCTAssertEqual(1, try storage.fetchAllObjects(descending: false)?[0].id) 82 | } 83 | 84 | func testObjectExpiry() { 85 | storage = initStorageWith(injectedDate: Date(), expiryDate: .seconds(interval: -10)) 86 | 87 | storage.save(Post.dummyPosts()) 88 | XCTAssertTrue(storage.isExpired(for: 1)) 89 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count) 90 | } 91 | 92 | func testFetchStoredInLast() { 93 | 94 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4)) 95 | storage.save(Post.dummyPosts()) 96 | 97 | XCTAssertEqual(10, try storage.fetchObjectsStored(inLast: .minutes(interval: 5)).count) 98 | XCTAssertEqual(0, try storage.fetchObjectsStored(before: .minutes(interval: 5)).count) 99 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 5))[0]) 100 | } 101 | 102 | func testfetchStoredBefore() { 103 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4)) 104 | storage.save(Post.dummyPosts()) 105 | 106 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count) 107 | XCTAssertEqual(10, try storage.fetchObjectsStored(before: .minutes(interval: 3)).count) 108 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3))[0]) 109 | } 110 | 111 | func testSubscript() { 112 | storage.save(Post.dummyPosts()) 113 | XCTAssertTrue(storage[1]?.id == 1 ) 114 | XCTAssertFalse(storage[3]?.title == "Swift Article1") 115 | storage[1] = nil 116 | XCTAssertNil(storage[1]) 117 | } 118 | 119 | func testDeleteSingleObject() { 120 | storage.save(Post.singlePost) 121 | 122 | XCTAssertEqual(1, storage.objectsCount) 123 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id)) 124 | XCTAssertNil(storage[0]) 125 | } 126 | 127 | func testDeleteMultiObjects() { 128 | storage.save(Post.dummyPosts()) 129 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3])) 130 | XCTAssertEqual(7, storage.objectsCount) 131 | XCTAssertNil(storage[3]) 132 | } 133 | 134 | func testDeleteAllObjects() { 135 | storage.save(Post.dummyPosts()) 136 | XCTAssertEqual(10, storage.objectsCount) 137 | XCTAssertNoThrow(try storage.deleteAll()) 138 | XCTAssertEqual(0, storage.objectsCount) 139 | } 140 | 141 | func testStorageContains() { 142 | 143 | storage.save(Post.dummyPosts()) 144 | XCTAssert(storage.contains(2)) 145 | XCTAssertFalse(storage.contains(12)) 146 | } 147 | 148 | func testDeleteExpired() { 149 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 10), expiryDate: .minutes(interval: -4)) 150 | storage.save(Post.dummyPosts()) 151 | 152 | storage.deleteExpired() 153 | XCTAssertEqual(0, storage.objectsCount) 154 | } 155 | 156 | func testStorageCapacity() { 157 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4), maxObjectsCount: 5) 158 | storage.save(Post.dummyPosts()) 159 | XCTAssertEqual(5, storage.objectsCount) 160 | } 161 | 162 | private func initStorageWith(injectedDate: Date, 163 | expiryDate: SorageDateDescriptor = .minutes(interval: 4), 164 | maxObjectsCount: Int = 10) -> MemoryStorage{ 165 | 166 | return MemoryStorage(dateProvider: {return injectedDate}, 167 | expiryDate: expiryDate, 168 | maxObjectsCount: maxObjectsCount) 169 | } 170 | 171 | 172 | } 173 | 174 | -------------------------------------------------------------------------------- /Tests/Mock/PostMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | @testable import CodablePersist 25 | import Foundation 26 | 27 | struct Post: Identifiable, Codable { 28 | static var idKey = \Post.id 29 | var title: String 30 | var id: Int 31 | 32 | static func dummyPosts() -> [Post] { 33 | var reqs: [Post] = [] 34 | for i in 1...10 { 35 | let re = Post(title: "Swift Article\(i)", id: i) 36 | reqs.append(re) 37 | } 38 | return reqs 39 | 40 | } 41 | 42 | static func dummyPosts2() -> [Post] { 43 | var reqs: [Post] = [] 44 | for i in 1...10 { 45 | let re = Post(title: "Swift Article1\(i)", id: 10 + i) 46 | reqs.append(re) 47 | } 48 | return reqs 49 | 50 | } 51 | 52 | static let singlePost = Post(title: "Single post test", id: 0) 53 | } 54 | -------------------------------------------------------------------------------- /Tests/UserDefaultsStorageTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersist 3 | // 4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | 25 | import XCTest 26 | @testable import CodablePersist 27 | 28 | class UserDefaultsStorageTest: XCTestCase { 29 | 30 | var storage: UserDefaultsStorage! 31 | let storageName = "userdefaultsStorage" 32 | 33 | override func setUp() { 34 | storage = UserDefaultsStorage(storeName: storageName) 35 | super.setUp() 36 | } 37 | 38 | override func tearDown() { 39 | try? storage.deleteAll() 40 | storage = nil 41 | super.tearDown() 42 | } 43 | 44 | func testInitStorage() { 45 | storage = UserDefaultsStorage(storeName: storageName) 46 | XCTAssertNotNil(storage) 47 | XCTAssertEqual(storage.storeName, storageName) 48 | } 49 | 50 | func testSaveSingleObject() { 51 | 52 | XCTAssertNoThrow( try storage.save(Post.singlePost)) 53 | XCTAssertEqual(storage.objectsCount, 1) 54 | XCTAssertTrue(storage.contains(0)) 55 | } 56 | 57 | func testSaveMultiObjects() { 58 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 59 | XCTAssertEqual(10, storage.objectsCount) 60 | XCTAssertTrue(storage.contains(4)) 61 | XCTAssertEqual([1,2,3],[ storage[1]?.id, storage[2]?.id, storage[3]?.id]) 62 | } 63 | 64 | func testFetchSingleObject() { 65 | XCTAssertNoThrow(try storage.save(Post.singlePost)) 66 | XCTAssertNotNil(try storage.fetchObject(for: 0) ) 67 | XCTAssertEqual(0, try storage.fetchObject(for: 0)?.id) 68 | XCTAssertEqual("Single post test", try storage.fetchObject(for: 0)?.title) 69 | } 70 | 71 | 72 | func testFetchMultiObjects() { 73 | 74 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 75 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) ) 76 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title) 77 | } 78 | 79 | func testFetchAllObjects() { 80 | 81 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 82 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count) 83 | XCTAssertEqual(1, try storage.fetchAllObjects(descending: false)?[0].id) 84 | } 85 | 86 | func testObjectExpiry() { 87 | storage = initStorageWith(injectedDate: Date(), expiryDate: .seconds(interval: -10)) 88 | 89 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 90 | XCTAssertTrue(storage.isExpired(for: 1)) 91 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count) 92 | } 93 | 94 | func testFetchStoredInLast() { 95 | 96 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4)) 97 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 98 | 99 | 100 | XCTAssertEqual(10, try storage.fetchObjectsStored(inLast: .minutes(interval: 5)).count) 101 | XCTAssertEqual(0, try storage.fetchObjectsStored(before: .minutes(interval: 5)).count) 102 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 5))[0]) 103 | } 104 | 105 | func testfetchStoredBefore() { 106 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4)) 107 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 108 | 109 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count) 110 | XCTAssertEqual(10, try storage.fetchObjectsStored(before: .minutes(interval: 3)).count) 111 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3))[0]) 112 | } 113 | 114 | func testSubscript() { 115 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 116 | XCTAssertTrue(storage[1]?.id == 1 ) 117 | XCTAssertFalse(storage[3]?.title == "Swift Article1") 118 | storage[1] = nil 119 | XCTAssertNil(storage[1]) 120 | } 121 | 122 | func testDeleteSingleObject() { 123 | XCTAssertNoThrow(try storage.save(Post.singlePost)) 124 | 125 | XCTAssertEqual(1, storage.objectsCount) 126 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id)) 127 | XCTAssertNil(storage[0]) 128 | } 129 | 130 | func testDeleteMultiObjects() { 131 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 132 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3])) 133 | XCTAssertEqual(7, storage.objectsCount) 134 | XCTAssertNil(storage[3]) 135 | } 136 | 137 | func testDeleteAllObjects() { 138 | 139 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 140 | XCTAssertEqual(10, storage.objectsCount) 141 | XCTAssertNoThrow(try storage.deleteAll()) 142 | XCTAssertEqual(0, storage.objectsCount) 143 | } 144 | 145 | func testStorageContains() { 146 | 147 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 148 | XCTAssert(storage.contains(2)) 149 | XCTAssertFalse(storage.contains(12)) 150 | } 151 | 152 | func testDeleteExpired() { 153 | 154 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 10), expiryDate: .minutes(interval: -4)) 155 | XCTAssertNoThrow(try storage.save(Post.dummyPosts())) 156 | 157 | storage.deleteExpired() 158 | XCTAssertEqual(0, storage.objectsCount) 159 | } 160 | 161 | private func initStorageWith(injectedDate: Date, 162 | expiryDate: SorageDateDescriptor = .minutes(interval: 6)) -> UserDefaultsStorage{ 163 | 164 | return UserDefaultsStorage(storeName: storageName, dateProvider: {return injectedDate},expiryDate: expiryDate)! 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /docs/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Classes

86 |

The following classes are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | DiskStorage 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Disk Storage utilizes FileManager to cache and retrive data to and from the disk 106 | it gives a convenient and easy to use way to cache any Codable and Identifiable object.

    107 | 108 | See more 109 |
    110 |
    111 |

    Declaration

    112 |
    113 |

    Swift

    114 |
    public class DiskStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    115 |
    extension DiskStorage: CacheStorable
    116 | 117 |
    118 |
    119 |
    120 |
    121 |
  • 122 |
  • 123 |
    124 | 125 | 126 | 127 | MemoryStorage 128 | 129 |
    130 |
    131 |
    132 |
    133 |
    134 |
    135 |

    Memory Storage utilizes NSCache to cache and retrive data to and from memory it gives a 136 | convenient and easy to use way to cache any Codable and Identifiable object. 137 | WARNING: Don’t use it for heavy objects as there will be potential lose of date if any 138 | memory pressure happens. 139 | Consider using DiskStorage for heavy and long term persistence.

    140 | 141 | See more 142 |
    143 |
    144 |

    Declaration

    145 |
    146 |

    Swift

    147 |
    final public class MemoryStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    148 |
    extension MemoryStorage: CacheStorable
    149 | 150 |
    151 |
    152 |
    153 |
    154 |
  • 155 |
  • 156 |
    157 | 158 | 159 | 160 | UserDefaultsStorage 161 | 162 |
    163 |
    164 |
    165 |
    166 |
    167 |
    168 |

    User Defaults Storage utilizes UserDefaults to cache and retrive data to and from 169 | UserDefaluts it gives a convenient and easy to use way to cache any Codable and Identifiable 170 | object. WARNING: Don’t use it for heavy and large number of objects 171 | Consider using DiskStorage for heavy and long term persistence.

    172 | 173 | See more 174 |
    175 |
    176 |

    Declaration

    177 |
    178 |

    Swift

    179 |
    public class UserDefaultsStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    180 |
    extension UserDefaultsStorage: CacheStorable
    181 | 182 |
    183 |
    184 |
    185 |
    186 |
  • 187 |
188 |
189 |
190 |
191 | 195 |
196 |
197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /docs/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Enumerations

86 |

The following enumerations are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | SorageDateDescriptor 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Sorage Date Descriptor

    106 | 107 |

    Describes object’s expiry date as well as a time snapshot used in fetching 108 | objects based on a specific time interval

    109 | 110 | See more 111 |
    112 |
    113 |

    Declaration

    114 |
    115 |

    Swift

    116 |
    public enum SorageDateDescriptor
    117 | 118 |
    119 |
    120 |
    121 |
    122 |
  • 123 |
124 |
125 |
126 |
127 | 131 |
132 |
133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/Protocols/Identifiable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identifiable Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Identifiable

86 |
87 |
88 |
public protocol Identifiable
89 | 90 |
91 |
92 |

A protocol conformed by objects that can be uniuqely identifed.

93 | 94 |
95 |
96 |
97 |
    98 |
  • 99 |
    100 | 101 | 102 | 103 | ID 104 | 105 |
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |

    Undocumented

    112 | 113 |
    114 |
    115 |

    Declaration

    116 |
    117 |

    Swift

    118 |
    associatedtype ID : Decodable, Encodable, Hashable
    119 | 120 |
    121 |
    122 |
    123 |
    124 |
  • 125 |
  • 126 |
    127 | 128 | 129 | 130 | idKey 131 | 132 |
    133 |
    134 |
    135 |
    136 |
    137 |
    138 |

    a KeyPath to the uniuqe id.

    139 | 140 |
    141 |
    142 |

    Declaration

    143 |
    144 |

    Swift

    145 |
    static var idKey: WritableKeyPath<Self, ID> { get }
    146 | 147 |
    148 |
    149 |
    150 |
    151 |
  • 152 |
153 |
154 |
155 |
156 | 160 |
161 |
162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/Structs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Structures Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Structures

86 |

The following structures are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | ObjectDescriptor 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Object Descriptor

    106 | 107 |

    Describe an object that need to be stored and retrieved.

    108 |
    109 |

    Seealso

    110 | MemoryStorage.Entry 111 | 112 |
    113 | 114 |
    115 |
    116 |

    Declaration

    117 |
    118 |

    Swift

    119 |
    public struct ObjectDescriptor<T> where T : Identifiable, T : Decodable, T : Encodable
    120 |
    extension ObjectDescriptor: Codable
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 95% 23 | 24 | 25 | 95% 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | a { 60 | color: #0088cc; 61 | text-decoration: none; } 62 | 63 | ul { 64 | padding-left: 15px; } 65 | 66 | li { 67 | line-height: 1.8em; } 68 | 69 | img { 70 | max-width: 100%; } 71 | 72 | blockquote { 73 | margin-left: 0; 74 | padding: 0 10px; 75 | border-left: 4px solid #ccc; } 76 | 77 | .content-wrapper { 78 | margin: 0 auto; 79 | width: 980px; } 80 | 81 | header { 82 | font-size: 0.85em; 83 | line-height: 26px; 84 | background-color: #414141; 85 | position: fixed; 86 | width: 100%; 87 | z-index: 2; } 88 | header img { 89 | padding-right: 6px; 90 | vertical-align: -4px; 91 | height: 16px; } 92 | header a { 93 | color: #fff; } 94 | header p { 95 | float: left; 96 | color: #999; } 97 | header .header-right { 98 | float: right; 99 | margin-left: 16px; } 100 | 101 | #breadcrumbs { 102 | background-color: #f2f2f2; 103 | height: 27px; 104 | padding-top: 17px; 105 | position: fixed; 106 | width: 100%; 107 | z-index: 2; 108 | margin-top: 26px; } 109 | #breadcrumbs #carat { 110 | height: 10px; 111 | margin: 0 5px; } 112 | 113 | .sidebar { 114 | background-color: #f9f9f9; 115 | border: 1px solid #e2e2e2; 116 | overflow-y: auto; 117 | overflow-x: hidden; 118 | position: fixed; 119 | top: 70px; 120 | bottom: 0; 121 | width: 230px; 122 | word-wrap: normal; } 123 | 124 | .nav-groups { 125 | list-style-type: none; 126 | background: #fff; 127 | padding-left: 0; } 128 | 129 | .nav-group-name { 130 | border-bottom: 1px solid #e2e2e2; 131 | font-size: 1.1em; 132 | font-weight: 100; 133 | padding: 15px 0 15px 20px; } 134 | .nav-group-name > a { 135 | color: #333; } 136 | 137 | .nav-group-tasks { 138 | margin-top: 5px; } 139 | 140 | .nav-group-task { 141 | font-size: 0.9em; 142 | list-style-type: none; 143 | white-space: nowrap; } 144 | .nav-group-task a { 145 | color: #888; } 146 | 147 | .main-content { 148 | background-color: #fff; 149 | border: 1px solid #e2e2e2; 150 | margin-left: 246px; 151 | position: absolute; 152 | overflow: hidden; 153 | padding-bottom: 20px; 154 | top: 70px; 155 | width: 734px; } 156 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 157 | margin-bottom: 1em; } 158 | .main-content p { 159 | line-height: 1.8em; } 160 | .main-content section .section:first-child { 161 | margin-top: 0; 162 | padding-top: 0; } 163 | .main-content section .task-group-section .task-group:first-of-type { 164 | padding-top: 10px; } 165 | .main-content section .task-group-section .task-group:first-of-type .section-name { 166 | padding-top: 15px; } 167 | .main-content section .heading:before { 168 | content: ""; 169 | display: block; 170 | padding-top: 70px; 171 | margin: -70px 0 0; } 172 | .main-content .section-name p { 173 | margin-bottom: inherit; 174 | line-height: inherit; } 175 | .main-content .section-name code { 176 | background-color: inherit; 177 | padding: inherit; 178 | color: inherit; } 179 | 180 | .section { 181 | padding: 0 25px; } 182 | 183 | .highlight { 184 | background-color: #eee; 185 | padding: 10px 12px; 186 | border: 1px solid #e2e2e2; 187 | border-radius: 4px; 188 | overflow-x: auto; } 189 | 190 | .declaration .highlight { 191 | overflow-x: initial; 192 | padding: 0 40px 40px 0; 193 | margin-bottom: -25px; 194 | background-color: transparent; 195 | border: none; } 196 | 197 | .section-name { 198 | margin: 0; 199 | margin-left: 18px; } 200 | 201 | .task-group-section { 202 | padding-left: 6px; 203 | border-top: 1px solid #e2e2e2; } 204 | 205 | .task-group { 206 | padding-top: 0px; } 207 | 208 | .task-name-container a[name]:before { 209 | content: ""; 210 | display: block; 211 | padding-top: 70px; 212 | margin: -70px 0 0; } 213 | 214 | .section-name-container { 215 | position: relative; 216 | display: inline-block; } 217 | .section-name-container .section-name-link { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | bottom: 0; 222 | right: 0; 223 | margin-bottom: 0; } 224 | .section-name-container .section-name { 225 | position: relative; 226 | pointer-events: none; 227 | z-index: 1; } 228 | .section-name-container .section-name a { 229 | pointer-events: auto; } 230 | 231 | .item { 232 | padding-top: 8px; 233 | width: 100%; 234 | list-style-type: none; } 235 | .item a[name]:before { 236 | content: ""; 237 | display: block; 238 | padding-top: 70px; 239 | margin: -70px 0 0; } 240 | .item code { 241 | background-color: transparent; 242 | padding: 0; } 243 | .item .token, .item .direct-link { 244 | display: inline-block; 245 | text-indent: -20px; 246 | padding-left: 3px; 247 | margin-left: 35px; 248 | font-size: 11.9px; 249 | transition: all 300ms; } 250 | .item .token-open { 251 | margin-left: 20px; } 252 | .item .discouraged { 253 | text-decoration: line-through; } 254 | .item .declaration-note { 255 | font-size: .85em; 256 | color: gray; 257 | font-style: italic; } 258 | 259 | .pointer-container { 260 | border-bottom: 1px solid #e2e2e2; 261 | left: -23px; 262 | padding-bottom: 13px; 263 | position: relative; 264 | width: 110%; } 265 | 266 | .pointer { 267 | background: #f9f9f9; 268 | border-left: 1px solid #e2e2e2; 269 | border-top: 1px solid #e2e2e2; 270 | height: 12px; 271 | left: 21px; 272 | top: -7px; 273 | -webkit-transform: rotate(45deg); 274 | -moz-transform: rotate(45deg); 275 | -o-transform: rotate(45deg); 276 | transform: rotate(45deg); 277 | position: absolute; 278 | width: 12px; } 279 | 280 | .height-container { 281 | display: none; 282 | left: -25px; 283 | padding: 0 25px; 284 | position: relative; 285 | width: 100%; 286 | overflow: hidden; } 287 | .height-container .section { 288 | background: #f9f9f9; 289 | border-bottom: 1px solid #e2e2e2; 290 | left: -25px; 291 | position: relative; 292 | width: 100%; 293 | padding-top: 10px; 294 | padding-bottom: 5px; } 295 | 296 | .aside, .language { 297 | padding: 6px 12px; 298 | margin: 12px 0; 299 | border-left: 5px solid #dddddd; 300 | overflow-y: hidden; } 301 | .aside .aside-title, .language .aside-title { 302 | font-size: 9px; 303 | letter-spacing: 2px; 304 | text-transform: uppercase; 305 | padding-bottom: 0; 306 | margin: 0; 307 | color: #aaa; 308 | -webkit-user-select: none; } 309 | .aside p:last-child, .language p:last-child { 310 | margin-bottom: 0; } 311 | 312 | .language { 313 | border-left: 5px solid #cde9f4; } 314 | .language .aside-title { 315 | color: #4b8afb; } 316 | 317 | .aside-warning, .aside-deprecated, .aside-unavailable { 318 | border-left: 5px solid #ff6666; } 319 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 320 | color: #ff0000; } 321 | 322 | .graybox { 323 | border-collapse: collapse; 324 | width: 100%; } 325 | .graybox p { 326 | margin: 0; 327 | word-break: break-word; 328 | min-width: 50px; } 329 | .graybox td { 330 | border: 1px solid #e2e2e2; 331 | padding: 5px 25px 5px 10px; 332 | vertical-align: middle; } 333 | .graybox tr td:first-of-type { 334 | text-align: right; 335 | padding: 7px; 336 | vertical-align: top; 337 | word-break: normal; 338 | width: 40px; } 339 | 340 | .slightly-smaller { 341 | font-size: 0.9em; } 342 | 343 | #footer { 344 | position: relative; 345 | top: 10px; 346 | bottom: 0px; 347 | margin-left: 25px; } 348 | #footer p { 349 | margin: 0; 350 | color: #aaa; 351 | font-size: 0.8em; } 352 | 353 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 354 | display: none; } 355 | 356 | html.dash .main-content { 357 | width: 980px; 358 | margin-left: 0; 359 | border: none; 360 | width: 100%; 361 | top: 0; 362 | padding-bottom: 0; } 363 | 364 | html.dash .height-container { 365 | display: block; } 366 | 367 | html.dash .item .token { 368 | margin-left: 0; } 369 | 370 | html.dash .content-wrapper { 371 | width: auto; } 372 | 373 | html.dash #footer { 374 | position: static; } 375 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.jazzy.codablepersist 7 | CFBundleName 8 | CodablePersist 9 | DocSetPlatformFamily 10 | codablepersist 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetFamily 18 | dashtoc 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/Classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Classes Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Classes

86 |

The following classes are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | DiskStorage 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Disk Storage utilizes FileManager to cache and retrive data to and from the disk 106 | it gives a convenient and easy to use way to cache any Codable and Identifiable object.

    107 | 108 | See more 109 |
    110 |
    111 |

    Declaration

    112 |
    113 |

    Swift

    114 |
    public class DiskStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    115 |
    extension DiskStorage: CacheStorable
    116 | 117 |
    118 |
    119 |
    120 |
    121 |
  • 122 |
  • 123 |
    124 | 125 | 126 | 127 | MemoryStorage 128 | 129 |
    130 |
    131 |
    132 |
    133 |
    134 |
    135 |

    Memory Storage utilizes NSCache to cache and retrive data to and from memory it gives a 136 | convenient and easy to use way to cache any Codable and Identifiable object. 137 | WARNING: Don’t use it for heavy objects as there will be potential lose of date if any 138 | memory pressure happens. 139 | Consider using DiskStorage for heavy and long term persistence.

    140 | 141 | See more 142 |
    143 |
    144 |

    Declaration

    145 |
    146 |

    Swift

    147 |
    final public class MemoryStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    148 |
    extension MemoryStorage: CacheStorable
    149 | 150 |
    151 |
    152 |
    153 |
    154 |
  • 155 |
  • 156 |
    157 | 158 | 159 | 160 | UserDefaultsStorage 161 | 162 |
    163 |
    164 |
    165 |
    166 |
    167 |
    168 |

    User Defaults Storage utilizes UserDefaults to cache and retrive data to and from 169 | UserDefaluts it gives a convenient and easy to use way to cache any Codable and Identifiable 170 | object. WARNING: Don’t use it for heavy and large number of objects 171 | Consider using DiskStorage for heavy and long term persistence.

    172 | 173 | See more 174 |
    175 |
    176 |

    Declaration

    177 |
    178 |

    Swift

    179 |
    public class UserDefaultsStorage<T> where T : Identifiable, T : Decodable, T : Encodable
    180 |
    extension UserDefaultsStorage: CacheStorable
    181 | 182 |
    183 |
    184 |
    185 |
    186 |
  • 187 |
188 |
189 |
190 |
191 | 195 |
196 |
197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/Enums.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enumerations Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Enumerations

86 |

The following enumerations are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | SorageDateDescriptor 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Sorage Date Descriptor

    106 | 107 |

    Describes object’s expiry date as well as a time snapshot used in fetching 108 | objects based on a specific time interval

    109 | 110 | See more 111 |
    112 |
    113 |

    Declaration

    114 |
    115 |

    Swift

    116 |
    public enum SorageDateDescriptor
    117 | 118 |
    119 |
    120 |
    121 |
    122 |
  • 123 |
124 |
125 |
126 |
127 | 131 |
132 |
133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/Protocols/Identifiable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Identifiable Protocol Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Identifiable

86 |
87 |
88 |
public protocol Identifiable
89 | 90 |
91 |
92 |

A protocol conformed by objects that can be uniuqely identifed.

93 | 94 |
95 |
96 |
97 |
    98 |
  • 99 |
    100 | 101 | 102 | 103 | ID 104 | 105 |
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |

    Undocumented

    112 | 113 |
    114 |
    115 |

    Declaration

    116 |
    117 |

    Swift

    118 |
    associatedtype ID : Decodable, Encodable, Hashable
    119 | 120 |
    121 |
    122 |
    123 |
    124 |
  • 125 |
  • 126 |
    127 | 128 | 129 | 130 | idKey 131 | 132 |
    133 |
    134 |
    135 |
    136 |
    137 |
    138 |

    a KeyPath to the uniuqe id.

    139 | 140 |
    141 |
    142 |

    Declaration

    143 |
    144 |

    Swift

    145 |
    static var idKey: WritableKeyPath<Self, ID> { get }
    146 | 147 |
    148 |
    149 |
    150 |
    151 |
  • 152 |
153 |
154 |
155 |
156 | 160 |
161 |
162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/Structs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Structures Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

CodablePersist Docs (95% documented)

18 |
19 |
20 |
21 | 26 |
27 |
28 | 82 |
83 |
84 |
85 |

Structures

86 |

The following structures are available globally.

87 | 88 |
89 |
90 |
91 |
    92 |
  • 93 |
    94 | 95 | 96 | 97 | ObjectDescriptor 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |
    105 |

    Object Descriptor

    106 | 107 |

    Describe an object that need to be stored and retrieved.

    108 |
    109 |

    Seealso

    110 | MemoryStorage.Entry 111 | 112 |
    113 | 114 |
    115 |
    116 |

    Declaration

    117 |
    118 |

    Swift

    119 |
    public struct ObjectDescriptor<T> where T : Identifiable, T : Decodable, T : Encodable
    120 |
    extension ObjectDescriptor: Codable
    121 | 122 |
    123 |
    124 |
    125 |
    126 |
  • 127 |
128 |
129 |
130 |
131 | 135 |
136 |
137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* Credit to https://gist.github.com/wataru420/2048287 */ 2 | .highlight { 3 | /* Comment */ 4 | /* Error */ 5 | /* Keyword */ 6 | /* Operator */ 7 | /* Comment.Multiline */ 8 | /* Comment.Preproc */ 9 | /* Comment.Single */ 10 | /* Comment.Special */ 11 | /* Generic.Deleted */ 12 | /* Generic.Deleted.Specific */ 13 | /* Generic.Emph */ 14 | /* Generic.Error */ 15 | /* Generic.Heading */ 16 | /* Generic.Inserted */ 17 | /* Generic.Inserted.Specific */ 18 | /* Generic.Output */ 19 | /* Generic.Prompt */ 20 | /* Generic.Strong */ 21 | /* Generic.Subheading */ 22 | /* Generic.Traceback */ 23 | /* Keyword.Constant */ 24 | /* Keyword.Declaration */ 25 | /* Keyword.Pseudo */ 26 | /* Keyword.Reserved */ 27 | /* Keyword.Type */ 28 | /* Literal.Number */ 29 | /* Literal.String */ 30 | /* Name.Attribute */ 31 | /* Name.Builtin */ 32 | /* Name.Class */ 33 | /* Name.Constant */ 34 | /* Name.Entity */ 35 | /* Name.Exception */ 36 | /* Name.Function */ 37 | /* Name.Namespace */ 38 | /* Name.Tag */ 39 | /* Name.Variable */ 40 | /* Operator.Word */ 41 | /* Text.Whitespace */ 42 | /* Literal.Number.Float */ 43 | /* Literal.Number.Hex */ 44 | /* Literal.Number.Integer */ 45 | /* Literal.Number.Oct */ 46 | /* Literal.String.Backtick */ 47 | /* Literal.String.Char */ 48 | /* Literal.String.Doc */ 49 | /* Literal.String.Double */ 50 | /* Literal.String.Escape */ 51 | /* Literal.String.Heredoc */ 52 | /* Literal.String.Interpol */ 53 | /* Literal.String.Other */ 54 | /* Literal.String.Regex */ 55 | /* Literal.String.Single */ 56 | /* Literal.String.Symbol */ 57 | /* Name.Builtin.Pseudo */ 58 | /* Name.Variable.Class */ 59 | /* Name.Variable.Global */ 60 | /* Name.Variable.Instance */ 61 | /* Literal.Number.Integer.Long */ } 62 | .highlight .c { 63 | color: #999988; 64 | font-style: italic; } 65 | .highlight .err { 66 | color: #a61717; 67 | background-color: #e3d2d2; } 68 | .highlight .k { 69 | color: #000000; 70 | font-weight: bold; } 71 | .highlight .o { 72 | color: #000000; 73 | font-weight: bold; } 74 | .highlight .cm { 75 | color: #999988; 76 | font-style: italic; } 77 | .highlight .cp { 78 | color: #999999; 79 | font-weight: bold; } 80 | .highlight .c1 { 81 | color: #999988; 82 | font-style: italic; } 83 | .highlight .cs { 84 | color: #999999; 85 | font-weight: bold; 86 | font-style: italic; } 87 | .highlight .gd { 88 | color: #000000; 89 | background-color: #ffdddd; } 90 | .highlight .gd .x { 91 | color: #000000; 92 | background-color: #ffaaaa; } 93 | .highlight .ge { 94 | color: #000000; 95 | font-style: italic; } 96 | .highlight .gr { 97 | color: #aa0000; } 98 | .highlight .gh { 99 | color: #999999; } 100 | .highlight .gi { 101 | color: #000000; 102 | background-color: #ddffdd; } 103 | .highlight .gi .x { 104 | color: #000000; 105 | background-color: #aaffaa; } 106 | .highlight .go { 107 | color: #888888; } 108 | .highlight .gp { 109 | color: #555555; } 110 | .highlight .gs { 111 | font-weight: bold; } 112 | .highlight .gu { 113 | color: #aaaaaa; } 114 | .highlight .gt { 115 | color: #aa0000; } 116 | .highlight .kc { 117 | color: #000000; 118 | font-weight: bold; } 119 | .highlight .kd { 120 | color: #000000; 121 | font-weight: bold; } 122 | .highlight .kp { 123 | color: #000000; 124 | font-weight: bold; } 125 | .highlight .kr { 126 | color: #000000; 127 | font-weight: bold; } 128 | .highlight .kt { 129 | color: #445588; } 130 | .highlight .m { 131 | color: #009999; } 132 | .highlight .s { 133 | color: #d14; } 134 | .highlight .na { 135 | color: #008080; } 136 | .highlight .nb { 137 | color: #0086B3; } 138 | .highlight .nc { 139 | color: #445588; 140 | font-weight: bold; } 141 | .highlight .no { 142 | color: #008080; } 143 | .highlight .ni { 144 | color: #800080; } 145 | .highlight .ne { 146 | color: #990000; 147 | font-weight: bold; } 148 | .highlight .nf { 149 | color: #990000; } 150 | .highlight .nn { 151 | color: #555555; } 152 | .highlight .nt { 153 | color: #000080; } 154 | .highlight .nv { 155 | color: #008080; } 156 | .highlight .ow { 157 | color: #000000; 158 | font-weight: bold; } 159 | .highlight .w { 160 | color: #bbbbbb; } 161 | .highlight .mf { 162 | color: #009999; } 163 | .highlight .mh { 164 | color: #009999; } 165 | .highlight .mi { 166 | color: #009999; } 167 | .highlight .mo { 168 | color: #009999; } 169 | .highlight .sb { 170 | color: #d14; } 171 | .highlight .sc { 172 | color: #d14; } 173 | .highlight .sd { 174 | color: #d14; } 175 | .highlight .s2 { 176 | color: #d14; } 177 | .highlight .se { 178 | color: #d14; } 179 | .highlight .sh { 180 | color: #d14; } 181 | .highlight .si { 182 | color: #d14; } 183 | .highlight .sx { 184 | color: #d14; } 185 | .highlight .sr { 186 | color: #009926; } 187 | .highlight .s1 { 188 | color: #d14; } 189 | .highlight .ss { 190 | color: #990073; } 191 | .highlight .bp { 192 | color: #999999; } 193 | .highlight .vc { 194 | color: #008080; } 195 | .highlight .vg { 196 | color: #008080; } 197 | .highlight .vi { 198 | color: #008080; } 199 | .highlight .il { 200 | color: #009999; } 201 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/css/jazzy.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { 2 | background: transparent; 3 | border: 0; 4 | margin: 0; 5 | outline: 0; 6 | padding: 0; 7 | vertical-align: baseline; } 8 | 9 | body { 10 | background-color: #f2f2f2; 11 | font-family: Helvetica, freesans, Arial, sans-serif; 12 | font-size: 14px; 13 | -webkit-font-smoothing: subpixel-antialiased; 14 | word-wrap: break-word; } 15 | 16 | h1, h2, h3 { 17 | margin-top: 0.8em; 18 | margin-bottom: 0.3em; 19 | font-weight: 100; 20 | color: black; } 21 | 22 | h1 { 23 | font-size: 2.5em; } 24 | 25 | h2 { 26 | font-size: 2em; 27 | border-bottom: 1px solid #e2e2e2; } 28 | 29 | h4 { 30 | font-size: 13px; 31 | line-height: 1.5; 32 | margin-top: 21px; } 33 | 34 | h5 { 35 | font-size: 1.1em; } 36 | 37 | h6 { 38 | font-size: 1.1em; 39 | color: #777; } 40 | 41 | .section-name { 42 | color: gray; 43 | display: block; 44 | font-family: Helvetica; 45 | font-size: 22px; 46 | font-weight: 100; 47 | margin-bottom: 15px; } 48 | 49 | pre, code { 50 | font: 0.95em Menlo, monospace; 51 | color: #777; 52 | word-wrap: normal; } 53 | 54 | p code, li code { 55 | background-color: #eee; 56 | padding: 2px 4px; 57 | border-radius: 4px; } 58 | 59 | a { 60 | color: #0088cc; 61 | text-decoration: none; } 62 | 63 | ul { 64 | padding-left: 15px; } 65 | 66 | li { 67 | line-height: 1.8em; } 68 | 69 | img { 70 | max-width: 100%; } 71 | 72 | blockquote { 73 | margin-left: 0; 74 | padding: 0 10px; 75 | border-left: 4px solid #ccc; } 76 | 77 | .content-wrapper { 78 | margin: 0 auto; 79 | width: 980px; } 80 | 81 | header { 82 | font-size: 0.85em; 83 | line-height: 26px; 84 | background-color: #414141; 85 | position: fixed; 86 | width: 100%; 87 | z-index: 2; } 88 | header img { 89 | padding-right: 6px; 90 | vertical-align: -4px; 91 | height: 16px; } 92 | header a { 93 | color: #fff; } 94 | header p { 95 | float: left; 96 | color: #999; } 97 | header .header-right { 98 | float: right; 99 | margin-left: 16px; } 100 | 101 | #breadcrumbs { 102 | background-color: #f2f2f2; 103 | height: 27px; 104 | padding-top: 17px; 105 | position: fixed; 106 | width: 100%; 107 | z-index: 2; 108 | margin-top: 26px; } 109 | #breadcrumbs #carat { 110 | height: 10px; 111 | margin: 0 5px; } 112 | 113 | .sidebar { 114 | background-color: #f9f9f9; 115 | border: 1px solid #e2e2e2; 116 | overflow-y: auto; 117 | overflow-x: hidden; 118 | position: fixed; 119 | top: 70px; 120 | bottom: 0; 121 | width: 230px; 122 | word-wrap: normal; } 123 | 124 | .nav-groups { 125 | list-style-type: none; 126 | background: #fff; 127 | padding-left: 0; } 128 | 129 | .nav-group-name { 130 | border-bottom: 1px solid #e2e2e2; 131 | font-size: 1.1em; 132 | font-weight: 100; 133 | padding: 15px 0 15px 20px; } 134 | .nav-group-name > a { 135 | color: #333; } 136 | 137 | .nav-group-tasks { 138 | margin-top: 5px; } 139 | 140 | .nav-group-task { 141 | font-size: 0.9em; 142 | list-style-type: none; 143 | white-space: nowrap; } 144 | .nav-group-task a { 145 | color: #888; } 146 | 147 | .main-content { 148 | background-color: #fff; 149 | border: 1px solid #e2e2e2; 150 | margin-left: 246px; 151 | position: absolute; 152 | overflow: hidden; 153 | padding-bottom: 20px; 154 | top: 70px; 155 | width: 734px; } 156 | .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { 157 | margin-bottom: 1em; } 158 | .main-content p { 159 | line-height: 1.8em; } 160 | .main-content section .section:first-child { 161 | margin-top: 0; 162 | padding-top: 0; } 163 | .main-content section .task-group-section .task-group:first-of-type { 164 | padding-top: 10px; } 165 | .main-content section .task-group-section .task-group:first-of-type .section-name { 166 | padding-top: 15px; } 167 | .main-content section .heading:before { 168 | content: ""; 169 | display: block; 170 | padding-top: 70px; 171 | margin: -70px 0 0; } 172 | .main-content .section-name p { 173 | margin-bottom: inherit; 174 | line-height: inherit; } 175 | .main-content .section-name code { 176 | background-color: inherit; 177 | padding: inherit; 178 | color: inherit; } 179 | 180 | .section { 181 | padding: 0 25px; } 182 | 183 | .highlight { 184 | background-color: #eee; 185 | padding: 10px 12px; 186 | border: 1px solid #e2e2e2; 187 | border-radius: 4px; 188 | overflow-x: auto; } 189 | 190 | .declaration .highlight { 191 | overflow-x: initial; 192 | padding: 0 40px 40px 0; 193 | margin-bottom: -25px; 194 | background-color: transparent; 195 | border: none; } 196 | 197 | .section-name { 198 | margin: 0; 199 | margin-left: 18px; } 200 | 201 | .task-group-section { 202 | padding-left: 6px; 203 | border-top: 1px solid #e2e2e2; } 204 | 205 | .task-group { 206 | padding-top: 0px; } 207 | 208 | .task-name-container a[name]:before { 209 | content: ""; 210 | display: block; 211 | padding-top: 70px; 212 | margin: -70px 0 0; } 213 | 214 | .section-name-container { 215 | position: relative; 216 | display: inline-block; } 217 | .section-name-container .section-name-link { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | bottom: 0; 222 | right: 0; 223 | margin-bottom: 0; } 224 | .section-name-container .section-name { 225 | position: relative; 226 | pointer-events: none; 227 | z-index: 1; } 228 | .section-name-container .section-name a { 229 | pointer-events: auto; } 230 | 231 | .item { 232 | padding-top: 8px; 233 | width: 100%; 234 | list-style-type: none; } 235 | .item a[name]:before { 236 | content: ""; 237 | display: block; 238 | padding-top: 70px; 239 | margin: -70px 0 0; } 240 | .item code { 241 | background-color: transparent; 242 | padding: 0; } 243 | .item .token, .item .direct-link { 244 | display: inline-block; 245 | text-indent: -20px; 246 | padding-left: 3px; 247 | margin-left: 35px; 248 | font-size: 11.9px; 249 | transition: all 300ms; } 250 | .item .token-open { 251 | margin-left: 20px; } 252 | .item .discouraged { 253 | text-decoration: line-through; } 254 | .item .declaration-note { 255 | font-size: .85em; 256 | color: gray; 257 | font-style: italic; } 258 | 259 | .pointer-container { 260 | border-bottom: 1px solid #e2e2e2; 261 | left: -23px; 262 | padding-bottom: 13px; 263 | position: relative; 264 | width: 110%; } 265 | 266 | .pointer { 267 | background: #f9f9f9; 268 | border-left: 1px solid #e2e2e2; 269 | border-top: 1px solid #e2e2e2; 270 | height: 12px; 271 | left: 21px; 272 | top: -7px; 273 | -webkit-transform: rotate(45deg); 274 | -moz-transform: rotate(45deg); 275 | -o-transform: rotate(45deg); 276 | transform: rotate(45deg); 277 | position: absolute; 278 | width: 12px; } 279 | 280 | .height-container { 281 | display: none; 282 | left: -25px; 283 | padding: 0 25px; 284 | position: relative; 285 | width: 100%; 286 | overflow: hidden; } 287 | .height-container .section { 288 | background: #f9f9f9; 289 | border-bottom: 1px solid #e2e2e2; 290 | left: -25px; 291 | position: relative; 292 | width: 100%; 293 | padding-top: 10px; 294 | padding-bottom: 5px; } 295 | 296 | .aside, .language { 297 | padding: 6px 12px; 298 | margin: 12px 0; 299 | border-left: 5px solid #dddddd; 300 | overflow-y: hidden; } 301 | .aside .aside-title, .language .aside-title { 302 | font-size: 9px; 303 | letter-spacing: 2px; 304 | text-transform: uppercase; 305 | padding-bottom: 0; 306 | margin: 0; 307 | color: #aaa; 308 | -webkit-user-select: none; } 309 | .aside p:last-child, .language p:last-child { 310 | margin-bottom: 0; } 311 | 312 | .language { 313 | border-left: 5px solid #cde9f4; } 314 | .language .aside-title { 315 | color: #4b8afb; } 316 | 317 | .aside-warning, .aside-deprecated, .aside-unavailable { 318 | border-left: 5px solid #ff6666; } 319 | .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { 320 | color: #ff0000; } 321 | 322 | .graybox { 323 | border-collapse: collapse; 324 | width: 100%; } 325 | .graybox p { 326 | margin: 0; 327 | word-break: break-word; 328 | min-width: 50px; } 329 | .graybox td { 330 | border: 1px solid #e2e2e2; 331 | padding: 5px 25px 5px 10px; 332 | vertical-align: middle; } 333 | .graybox tr td:first-of-type { 334 | text-align: right; 335 | padding: 7px; 336 | vertical-align: top; 337 | word-break: normal; 338 | width: 40px; } 339 | 340 | .slightly-smaller { 341 | font-size: 0.9em; } 342 | 343 | #footer { 344 | position: relative; 345 | top: 10px; 346 | bottom: 0px; 347 | margin-left: 25px; } 348 | #footer p { 349 | margin: 0; 350 | color: #aaa; 351 | font-size: 0.8em; } 352 | 353 | html.dash header, html.dash #breadcrumbs, html.dash .sidebar { 354 | display: none; } 355 | 356 | html.dash .main-content { 357 | width: 980px; 358 | margin-left: 0; 359 | border: none; 360 | width: 100%; 361 | top: 0; 362 | padding-bottom: 0; } 363 | 364 | html.dash .height-container { 365 | display: block; } 366 | 367 | html.dash .item .token { 368 | margin-left: 0; } 369 | 370 | html.dash .content-wrapper { 371 | width: auto; } 372 | 373 | html.dash #footer { 374 | position: static; } 375 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/carat.png -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/dash.png -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/docsets/CodablePersist.docset/Contents/Resources/Documents/img/gh.png -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CodablePersist Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

CodablePersist Docs (95% documented)

17 |
18 |
19 |
20 | 25 |
26 |
27 | 81 |
82 |
83 |
84 | 85 |

CodablePersist

86 |

CodablePersist

87 |

Installation

88 |
pod 'CodablePersist'
 89 | 
90 |

Authors

91 | 92 |

Ali A. Hilal

93 | 94 |
95 |
96 | 100 |
101 |
102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/Documents/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/docsets/CodablePersist.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/CodablePersist.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/docsets/CodablePersist.tgz -------------------------------------------------------------------------------- /docs/img/carat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/img/carat.png -------------------------------------------------------------------------------- /docs/img/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/img/dash.png -------------------------------------------------------------------------------- /docs/img/gh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engali94/CodablePersist/85e0f27156cf3b69e40ee39a8c482c7a79390de9/docs/img/gh.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CodablePersist Reference 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

CodablePersist Docs (95% documented)

17 |
18 |
19 |
20 | 25 |
26 |
27 | 81 |
82 |
83 |
84 | 85 |

CodablePersist

86 |

CodablePersist

87 |

Installation

88 |
pod 'CodablePersist'
 89 | 
90 |

Authors

91 | 92 |

Ali A. Hilal

93 | 94 |
95 |
96 | 100 |
101 |
102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/js/jazzy.js: -------------------------------------------------------------------------------- 1 | window.jazzy = {'docset': false} 2 | if (typeof window.dash != 'undefined') { 3 | document.documentElement.className += ' dash' 4 | window.jazzy.docset = true 5 | } 6 | if (navigator.userAgent.match(/xcode/i)) { 7 | document.documentElement.className += ' xcode' 8 | window.jazzy.docset = true 9 | } 10 | 11 | function toggleItem($link, $content) { 12 | var animationDuration = 300; 13 | $link.toggleClass('token-open'); 14 | $content.slideToggle(animationDuration); 15 | } 16 | 17 | function itemLinkToContent($link) { 18 | return $link.parent().parent().next(); 19 | } 20 | 21 | // On doc load + hash-change, open any targetted item 22 | function openCurrentItemIfClosed() { 23 | if (window.jazzy.docset) { 24 | return; 25 | } 26 | var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); 27 | $content = itemLinkToContent($link); 28 | if ($content.is(':hidden')) { 29 | toggleItem($link, $content); 30 | } 31 | } 32 | 33 | $(openCurrentItemIfClosed); 34 | $(window).on('hashchange', openCurrentItemIfClosed); 35 | 36 | // On item link ('token') click, toggle its discussion 37 | $('.token').on('click', function(event) { 38 | if (window.jazzy.docset) { 39 | return; 40 | } 41 | var $link = $(this); 42 | toggleItem($link, itemLinkToContent($link)); 43 | 44 | // Keeps the document from jumping to the hash. 45 | var href = $link.attr('href'); 46 | if (history.pushState) { 47 | history.pushState({}, '', href); 48 | } else { 49 | location.hash = href; 50 | } 51 | event.preventDefault(); 52 | }); 53 | 54 | // Clicks on links to the current, closed, item need to open the item 55 | $("a:not('.token')").on('click', function() { 56 | if (location == this.href) { 57 | openCurrentItemIfClosed(); 58 | } 59 | }); 60 | 61 | // KaTeX rendering 62 | if ("katex" in window) { 63 | $($('.math').each( (_, element) => { 64 | katex.render(element.textContent, element, { 65 | displayMode: $(element).hasClass('m-block'), 66 | throwOnError: false, 67 | trust: true 68 | }); 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /docs/undocumented.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": [ 3 | { 4 | "file": "/Users/ali/Desktop/Projects/CodablePersist/Sources/Core/Identifiable.swift", 5 | "line": 27, 6 | "symbol": "Identifiable.ID", 7 | "symbol_kind": "source.lang.swift.decl.associatedtype", 8 | "warning": "undocumented" 9 | }, 10 | { 11 | "file": "/Users/ali/Desktop/Projects/CodablePersist/Sources/Core/StorageReadable.swift", 12 | "line": 26, 13 | "symbol": "StorageReadable.T", 14 | "symbol_kind": "source.lang.swift.decl.associatedtype", 15 | "warning": "undocumented" 16 | }, 17 | { 18 | "file": "/Users/ali/Desktop/Projects/CodablePersist/Sources/Core/StorageRemovable.swift", 19 | "line": 33, 20 | "symbol": "StorageRemovable.T", 21 | "symbol_kind": "source.lang.swift.decl.associatedtype", 22 | "warning": "undocumented" 23 | }, 24 | { 25 | "file": "/Users/ali/Desktop/Projects/CodablePersist/Sources/Core/StorageWritable.swift", 26 | "line": 31, 27 | "symbol": "StorageWritable.T", 28 | "symbol_kind": "source.lang.swift.decl.associatedtype", 29 | "warning": "undocumented" 30 | }, 31 | { 32 | "file": "/Users/ali/Desktop/Projects/CodablePersist/Sources/Storage/MemoryStorage.swift", 33 | "line": 38, 34 | "symbol": "MemoryStorage.init(dateProvider:expiryDate:maxObjectsCount:)", 35 | "symbol_kind": "source.lang.swift.decl.function.method.instance", 36 | "warning": "undocumented" 37 | } 38 | ], 39 | "source_directory": "/Users/ali/Desktop/Projects/CodablePersist" 40 | } -------------------------------------------------------------------------------- /fastlane/.env: -------------------------------------------------------------------------------- 1 | FASTLANE_SKIP_UPDATE_CHECK=1 -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "2.120.0" 2 | 3 | default_platform :ios 4 | 5 | platform :ios do 6 | 7 | desc "Release a new version of CodablePersist" 8 | lane :release do |options| 9 | # Ensure Git status is clean 10 | ensure_git_status_clean 11 | # Ensure Git branch is master 12 | ensure_git_branch(branch: 'master') 13 | # Perform Dependency-Manager compatibility tests 14 | compatibilityTests 15 | # Perform Tests 16 | tests 17 | # Retrieve Version from options 18 | version = options[:version] 19 | # Increment Version 20 | increment(version: version) 21 | # Add Git Tag 22 | add_git_tag(tag: version) 23 | # Push Git Tag 24 | push_git_tags() 25 | # Push Git commit 26 | push_to_git_remote() 27 | # Pod push / Pod trunk 28 | pod_push() 29 | end 30 | 31 | desc "Increment Version" 32 | lane :increment do |options| 33 | # Retrieve Version from options 34 | version = options[:version] 35 | # Set Podspec version 36 | version_bump_podspec( 37 | path: "CodablePersist.podspec", 38 | version_number: version 39 | ) 40 | # Set Framework plist version 41 | set_info_plist_value( 42 | path: "Configs/CodablePersist.plist", 43 | key: "CFBundleShortVersionString", 44 | value: version 45 | ) 46 | # Set Framework Tests plist version 47 | set_info_plist_value( 48 | path: "Configs/CodablePersistTests.plist", 49 | key: "CFBundleShortVersionString", 50 | value: version 51 | ) 52 | # Set Example plist version 53 | set_info_plist_value( 54 | path: "Example/Resources/Info.plist", 55 | key: "CFBundleShortVersionString", 56 | value: version 57 | ) 58 | # Commit modified files 59 | git_commit( 60 | path: [ 61 | "CodablePersist.podspec", 62 | "Configs/CodablePersist.plist", 63 | "Configs/CodablePersistTests.plist", 64 | "Example/Resources/Info.plist" 65 | ], 66 | message: "CodablePersist Version #{version} 🚀" 67 | ) 68 | end 69 | 70 | desc "Runs tests" 71 | lane :tests do 72 | # Perform iOS Tests 73 | scan( 74 | project: "CodablePersist.xcodeproj", 75 | scheme: "CodablePersist-iOS", 76 | clean: true 77 | ) 78 | # Perform tvOS Tests 79 | scan( 80 | project: "CodablePersist.xcodeproj", 81 | scheme: "CodablePersist-tvOS", 82 | clean: true 83 | ) 84 | # Perform macOS Tests 85 | spm(command: "test") 86 | # Delete SPM build artifacts 87 | spm(command: "clean") 88 | end 89 | 90 | desc "Run Dependency-Manager compatibility tests" 91 | lane :compatibilityTests do 92 | # Carthage build to ensure Carthage compatibility 93 | carthage( 94 | command: "build", 95 | no_skip_current: true, 96 | cache_builds: true 97 | ) 98 | # Pod lib lint to ensure CocoaPods compatibility 99 | pod_lib_lint(allow_warnings: true) 100 | # SPM Build to ensure Swift Package Manager compatibility 101 | spm(command: "build") 102 | # Delete SPM build artifacts 103 | spm(command: "clean") 104 | end 105 | 106 | end 107 | --------------------------------------------------------------------------------