Skip to content

Commit fdf7ef1

Browse files
authored
Merge pull request #190 from hyperoslo/update/readme
Update README
2 parents 1432835 + 64af552 commit fdf7ef1

File tree

5 files changed

+91
-49
lines changed

5 files changed

+91
-49
lines changed

Cache.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181
D27014AF20D12D84003B45C7 /* AsyncStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */; };
8282
D27014B020D12E37003B45C7 /* StorageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98881F695F9400CE8F68 /* StorageSupportTests.swift */; };
8383
D27014B120D12E38003B45C7 /* StorageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98881F695F9400CE8F68 /* StorageSupportTests.swift */; };
84+
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236F3191F6BEF73004EE01F /* StorageTests.swift */; };
85+
D27014B420D13E2C003B45C7 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236F3191F6BEF73004EE01F /* StorageTests.swift */; };
8486
D28897051F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
8587
D28897061F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
8688
D28897071F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
@@ -828,6 +830,7 @@
828830
files = (
829831
D2CF98201F69427C00CE8F68 /* TestCase+Extensions.swift in Sources */,
830832
D2CF98231F69427C00CE8F68 /* TestHelper.swift in Sources */,
833+
D27014B420D13E2C003B45C7 /* StorageTests.swift in Sources */,
831834
D27014A320D129A3003B45C7 /* DiskStorageTests.swift in Sources */,
832835
D27014A020D12870003B45C7 /* MemoryStorageTests.swift in Sources */,
833836
D2CF98261F69427C00CE8F68 /* User.swift in Sources */,
@@ -852,6 +855,7 @@
852855
D2CF987F1F69513800CE8F68 /* ImageWrapperTests.swift in Sources */,
853856
D2D4CC1A1FA3166900E4A2D5 /* MD5Tests.swift in Sources */,
854857
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */,
858+
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */,
855859
D28C9BAF1F67EF8300C180C1 /* UIImage+ExtensionsTests.swift in Sources */,
856860
D2CF987D1F69513800CE8F68 /* MemoryCapsuleTests.swift in Sources */,
857861
D27014A120D129A2003B45C7 /* DiskStorageTests.swift in Sources */,

README.md

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
* [Sync APIs](#sync-apis)
1919
* [Async APIS](#async-apis)
2020
* [Expiry date](#expiry-date)
21-
* [What about images?](#what-about-images)
2221
* [Handling JSON response](#handling-json-response)
22+
* [What about images?](#what-about-images)
2323
* [Installation](#installation)
2424
* [Author](#author)
2525
* [Contributing](#contributing)
@@ -36,33 +36,65 @@ with out-of-box implementations and great customization possibilities. `Cache` u
3636
## Key features
3737

3838
- [x] Work with Swift 4 `Codable`. Anything conforming to `Codable` will be saved and loaded easily by `Storage`.
39-
- [X] Disk storage by default. Optionally using `memory storage` to enable hybrid.
39+
- [x] Hybrid with memory and disk storage.
4040
- [X] Many options via `DiskConfig` and `MemoryConfig`.
4141
- [x] Support `expiry` and clean up of expired objects.
4242
- [x] Thread safe. Operations can be accessed from any queue.
4343
- [x] Sync by default. Also support Async APIs.
44-
- [X] Store images via `ImageWrapper`.
4544
- [x] Extensive unit test coverage and great documentation.
4645
- [x] iOS, tvOS and macOS support.
4746

4847
## Usage
4948

5049
### Storage
5150

52-
`Cache` is built based on [Chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), in which there are many processing objects, each knows how to do 1 task and delegates to the next one. But that's just implementation detail. All you need to know is `Storage`, it saves and loads `Codable` objects.
51+
`Cache` is built based on [Chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), in which there are many processing objects, each knows how to do 1 task and delegates to the next one, so can you compose Storages the way you like.
52+
53+
For now the following Storage are supported
54+
55+
- `MemoryStorage`: save object to memory.
56+
- `DiskStorage`: save object to disk.
57+
- `HybridStorage`: save object to memory and disk, so you get persistented object on disk, while fast access with in memory objects.
58+
- `SyncStorage`: blocking APIs, all read and write operations are scheduled in a serial queue, all sync manner.
59+
- `AsyncStorage`: non-blocking APIs, operations are scheduled in an internal queue for serial processing. No read and write should happen at the same time.
5360

54-
`Storage` has disk storage and an optional memory storage. Memory storage should be less time and memory consuming, while disk storage is used for content that outlives the application life-cycle, see it more like a convenient way to store user information that should persist across application launches.
61+
Although you can use those Storage at your discretion, you don't have to. Because we also provide a convenient `Storage` which uses `HybridStorage` under the hood, while exposes sync and async APIs through `SyncStorage` and `AsyncStorage`.
5562

56-
`DiskConfig` is required to set up disk storage. You can optionally pass `MemoryConfig` to use memory as front storage.
63+
All you need to do is to specify the configuration you want with `DiskConfig` and `MemoryConfig`. The default configurations are good to go, but you can customise a lot.
5764

5865

5966
```swift
6067
let diskConfig = DiskConfig(name: "Floppy")
6168
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
6269

63-
let storage = try? Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
70+
let storage = try? Storage(
71+
diskConfig: diskConfig,
72+
memoryConfig: memoryConfig,
73+
transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<User>
74+
)
75+
```
76+
77+
### Generic, Type safety and Transformer
78+
79+
All `Storage` now are generic by default, so you can get a type safety experience. Once you create a Storage, it has a type constraint that you don't need to specify type for each operation afterwards.
80+
81+
If you want to change the type, `Cache` offers `transform` functions, look for `Transformer` and `TransformerFactory` for built-in transformers.
82+
83+
```swift
84+
let storage: Storage<User> = ...
85+
storage.setObject(superman, forKey: "user")
86+
87+
let imageStorage = storage.transformImage() // Storage<UIImage>
88+
imageStorage.setObject(image, forKey: "image")
89+
90+
let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String>
91+
stringStorage.setObject("hello world", forKey: "string")
6492
```
6593

94+
Each transformation allows you to work with a specific type, however the underlying caching mechanism remains the same, you're working with the same `Storage`, just with different type annotation. You can also create different `Storage` for each type if you want.
95+
96+
`Transformer` is necessary because the need of serialising and deserialising objects to and from `Data` for disk persistency. `Cache` provides default `Transformer ` for `Data`, `Codable` and `UIImage/NSImage`
97+
6698
#### Codable types
6799

68100
`Storage` supports any objects that conform to [Codable](https://developer.apple.com/documentation/swift/codable) protocol. You can [make your own things conform to Codable](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) so that can be saved and loaded from `Storage`.
@@ -95,6 +127,8 @@ public enum StorageError: Error {
95127
case encodingFailed
96128
/// The storage has been deallocated
97129
case deallocated
130+
/// Fail to perform transformation to or from Data
131+
case transformerFail
98132
}
99133
```
100134

@@ -156,11 +190,11 @@ try? storage.setObject(data, forKey: "a bunch of bytes")
156190
try? storage.setObject(authorizeURL, forKey: "authorization URL")
157191

158192
// Load from storage
159-
let score = try? storage.object(ofType: Int.self, forKey: "score")
160-
let favoriteCharacter = try? storage.object(ofType: String.self, forKey: "my favorite city")
193+
let score = try? storage.object(forKey: "score")
194+
let favoriteCharacter = try? storage.object(forKey: "my favorite city")
161195

162196
// Check if an object exists
163-
let hasFavoriteCharacter = try? storage.existsObject(ofType: String.self, forKey: "my favorite city")
197+
let hasFavoriteCharacter = try? storage.existsObject(forKey: "my favorite city")
164198

165199
// Remove an object in storage
166200
try? storage.removeObject(forKey: "my favorite city")
@@ -177,7 +211,7 @@ try? storage.removeExpiredObjects()
177211
There is time you want to get object together with its expiry information and meta data. You can use `Entry`
178212

179213
```swift
180-
let entry = try? storage.entry(ofType: String.self, forKey: "my favorite city")
214+
let entry = try? storage.entry(forKey: "my favorite city")
181215
print(entry?.object)
182216
print(entry?.expiry)
183217
print(entry?.meta)
@@ -215,7 +249,7 @@ storage.async.setObject("Oslo", forKey: "my favorite city") { result in
215249
}
216250
}
217251

218-
storage.async.object(ofType: String.self, forKey: "my favorite city") { result in
252+
storage.async.object(forKey: "my favorite city") { result in
219253
switch result {
220254
case .value(let city):
221255
print("my favorite city is \(city)")
@@ -224,7 +258,7 @@ storage.async.object(ofType: String.self, forKey: "my favorite city") { result i
224258
}
225259
}
226260

227-
storage.async.existsObject(ofType: String.self, forKey: "my favorite city") { result in
261+
storage.async.existsObject(forKey: "my favorite city") { result in
228262
if case .value(let exists) = result, exists {
229263
print("I have a favorite city")
230264
}
@@ -268,19 +302,6 @@ try? storage.setObject(
268302
storage.removeExpiredObjects()
269303
```
270304

271-
## What about images?
272-
273-
As you may know, `NSImage` and `UIImage` don't conform to `Codable` by default. To make it play well with `Codable`, we introduce `ImageWrapper`, so you can save and load images like
274-
275-
```swift
276-
let wrapper = ImageWrapper(image: starIconImage)
277-
try? storage.setObject(wrapper, forKey: "star")
278-
279-
let icon = try? storage.object(ofType: ImageWrapper.self, forKey: "star").image
280-
```
281-
282-
If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images.
283-
284305
## Handling JSON response
285306

286307
Most of the time, our use case is to fetch some json from backend, display it while saving the json to storage for future uses. If you're using libraries like [Alamofire](https://github.com/Alamofire/Alamofire) or [Malibu](https://github.com/hyperoslo/Malibu), you mostly get json in the form of dictionary, string, or data.
@@ -308,6 +329,10 @@ Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseStr
308329
}
309330
```
310331

332+
## What about images
333+
334+
If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images.
335+
311336
## Installation
312337

313338
### Cocoapods

Source/Shared/Storage/DiskStorage.swift

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,23 @@ final public class DiskStorage<T> {
1717

1818
// MARK: - Initialization
1919

20-
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
21-
self.config = config
22-
self.fileManager = fileManager
23-
self.transformer = transformer
24-
20+
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
2521
let url: URL
2622
if let directory = config.directory {
2723
url = directory
2824
} else {
2925
url = try fileManager.url(
30-
for: .documentDirectory,
26+
for: .cachesDirectory,
3127
in: .userDomainMask,
3228
appropriateFor: nil,
3329
create: true
3430
)
3531
}
3632

3733
// path
38-
self.path = url.appendingPathComponent(config.name, isDirectory: true).path
34+
let path = url.appendingPathComponent(config.name, isDirectory: true).path
35+
36+
self.init(config: config, fileManager: fileManager, path: path, transformer: transformer)
3937

4038
try createDirectory()
4139

@@ -48,6 +46,13 @@ final public class DiskStorage<T> {
4846
}
4947
#endif
5048
}
49+
50+
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<T>) {
51+
self.config = config
52+
self.fileManager = fileManager
53+
self.path = path
54+
self.transformer = transformer
55+
}
5156
}
5257

5358
extension DiskStorage: StorageAware {
@@ -239,10 +244,10 @@ extension DiskStorage {
239244

240245
public extension DiskStorage {
241246
func transform<U>(transformer: Transformer<U>) -> DiskStorage<U> {
242-
// swiftlint:disable force_try
243-
let storage = try! DiskStorage<U>(
247+
let storage = DiskStorage<U>(
244248
config: config,
245249
fileManager: fileManager,
250+
path: path,
246251
transformer: transformer
247252
)
248253

Tests/iOS/Tests/Storage/DiskStorageTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class DiskStorageTests: XCTestCase {
3030
/// Test that it returns the correct path
3131
func testDefaultPath() {
3232
let paths = NSSearchPathForDirectoriesInDomains(
33-
.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true
33+
.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true
3434
)
3535
let path = "\(paths.first!)/\(config.name.capitalized)"
3636
XCTAssertEqual(storage.path, path)

Tests/iOS/Tests/Storage/StorageTests.swift

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import XCTest
22
import Cache
33

44
final class StorageTests: XCTestCase {
5-
private var storage: Storage!
5+
private var storage: Storage<User>!
66
let user = User(firstName: "John", lastName: "Snow")
77

88
override func setUp() {
99
super.setUp()
1010

11-
storage = try! Storage(diskConfig: DiskConfig(name: "Thor"), memoryConfig: MemoryConfig())
11+
storage = try! Storage<User>(
12+
diskConfig: DiskConfig(name: "Thor"),
13+
memoryConfig: MemoryConfig(),
14+
transformer: TransformerFactory.forCodable(ofType: User.self)
15+
)
1216
}
1317

1418
override func tearDown() {
@@ -18,7 +22,7 @@ final class StorageTests: XCTestCase {
1822

1923
func testSync() throws {
2024
try storage.setObject(user, forKey: "user")
21-
let cachedObject = try storage.object(ofType: User.self, forKey: "user")
25+
let cachedObject = try storage.object(forKey: "user")
2226

2327
XCTAssertEqual(cachedObject, user)
2428
}
@@ -27,7 +31,7 @@ final class StorageTests: XCTestCase {
2731
let expectation = self.expectation(description: #function)
2832
storage.async.setObject(user, forKey: "user", expiry: nil, completion: { _ in })
2933

30-
storage.async.object(ofType: User.self, forKey: "user", completion: { result in
34+
storage.async.object(forKey: "user", completion: { result in
3135
switch result {
3236
case .value(let cachedUser):
3337
XCTAssertEqual(cachedUser, self.user)
@@ -50,20 +54,23 @@ final class StorageTests: XCTestCase {
5054
let lastName: String
5155
}
5256

57+
let person1Storage = storage.transformCodable(ofType: Person1.self)
58+
let person2Storage = storage.transformCodable(ofType: Person2.self)
59+
5360
// Firstly, save object of type Person1
5461
let person = Person1(fullName: "John Snow")
5562

56-
try! storage.setObject(person, forKey: "person")
57-
XCTAssertNil(try? storage.object(ofType: Person2.self, forKey: "person"))
63+
try! person1Storage.setObject(person, forKey: "person")
64+
XCTAssertNil(try? person2Storage.object(forKey: "person"))
5865

5966
// Later, convert to Person2, do the migration, then overwrite
60-
let tempPerson = try! storage.object(ofType: Person1.self, forKey: "person")
67+
let tempPerson = try! person1Storage.object(forKey: "person")
6168
let parts = tempPerson.fullName.split(separator: " ")
6269
let migratedPerson = Person2(firstName: String(parts[0]), lastName: String(parts[1]))
63-
try! storage.setObject(migratedPerson, forKey: "person")
70+
try! person2Storage.setObject(migratedPerson, forKey: "person")
6471

6572
XCTAssertEqual(
66-
try! storage.object(ofType: Person2.self, forKey: "person").firstName,
73+
try! person2Storage.object(forKey: "person").firstName,
6774
"John"
6875
)
6976
}
@@ -79,13 +86,14 @@ final class StorageTests: XCTestCase {
7986
let lastName: String
8087
}
8188

89+
let personStorage = storage.transformCodable(ofType: Person.self)
90+
let alienStorage = storage.transformCodable(ofType: Alien.self)
91+
8292
let person = Person(firstName: "John", lastName: "Snow")
83-
try! storage.setObject(person, forKey: "person")
93+
try! personStorage.setObject(person, forKey: "person")
8494

8595
// As long as it has same properties, it works too
86-
let cachedObject = try! storage.object(ofType: Alien.self, forKey: "person")
96+
let cachedObject = try! alienStorage.object(forKey: "person")
8797
XCTAssertEqual(cachedObject.firstName, "John")
8898
}
8999
}
90-
91-

0 commit comments

Comments
 (0)