Skip to content

Commit ac2a5bf

Browse files
authored
Merge pull request #104 from hyperoslo/feature/cache-array
Feature: cache array of Cachable objects
2 parents a696ede + dbcdeac commit ac2a5bf

File tree

9 files changed

+193
-20
lines changed

9 files changed

+193
-20
lines changed

Cache.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@
106106
D5C24DEF1EE8C50B00D2CF22 /* StringCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19B81EE8246F009F6AEE /* StringCacheTests.swift */; };
107107
D5C24DF01EE8C51500D2CF22 /* DiskStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19BC1EE8246F009F6AEE /* DiskStorageTests.swift */; };
108108
D5C24DF11EE8C51500D2CF22 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19BD1EE8246F009F6AEE /* MemoryStorageTests.swift */; };
109+
D5C24E221EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
110+
D5C24E231EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
111+
D5C24E241EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
112+
D5C24E261EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
113+
D5C24E271EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
114+
D5C24E281EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
109115
D5CE966D1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
110116
D5CE966E1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
111117
D5CE966F1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
@@ -196,6 +202,8 @@
196202
D5C24DD91EE8C29D00D2CF22 /* HybridCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HybridCacheTests.swift; sourceTree = "<group>"; };
197203
D5C24DDF1EE8C49800D2CF22 /* TestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = "<group>"; };
198204
D5C24DE31EE8C4E000D2CF22 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
205+
D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArray.swift; sourceTree = "<group>"; };
206+
D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArrayTests.swift; sourceTree = "<group>"; };
199207
D5CE966C1EE475C0002BFE06 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = "<group>"; };
200208
D5CE96851EE691D6002BFE06 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
201209
D5CE968A1EE69969002BFE06 /* SpecializedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecializedCache.swift; sourceTree = "<group>"; };
@@ -293,6 +301,7 @@
293301
D5CE966C1EE475C0002BFE06 /* Coding.swift */,
294302
D5291C191C28220B00B702C9 /* Expiry.swift */,
295303
D5291C1A1C28220B00B702C9 /* JSON.swift */,
304+
D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */,
296305
);
297306
path = DataStructures;
298307
sourceTree = "<group>";
@@ -400,6 +409,7 @@
400409
D59A19B01EE8246F009F6AEE /* CodingTests.swift */,
401410
D59A19B11EE8246F009F6AEE /* ExpiryTests.swift */,
402411
D59A19B21EE8246F009F6AEE /* JSONTests.swift */,
412+
D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */,
403413
);
404414
path = DataStructures;
405415
sourceTree = "<group>";
@@ -830,6 +840,7 @@
830840
buildActionMask = 2147483647;
831841
files = (
832842
BDEDD3651DBCE5D8007416A6 /* Expiry.swift in Sources */,
843+
D5C24E241EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
833844
BDEDD3631DBCE5D8007416A6 /* Cachable.swift in Sources */,
834845
BDEDD3661DBCE5D8007416A6 /* JSON.swift in Sources */,
835846
BDEDD3711DBCE5D8007416A6 /* StorageAware.swift in Sources */,
@@ -860,6 +871,7 @@
860871
D59A19DC1EE82484009F6AEE /* DataCacheTests.swift in Sources */,
861872
D59A19DE1EE82484009F6AEE /* JSONCacheTests.swift in Sources */,
862873
D59A19E01EE82484009F6AEE /* StringCacheTests.swift in Sources */,
874+
D5C24E281EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
863875
D59A19E41EE82484009F6AEE /* MemoryStorageTests.swift in Sources */,
864876
D59A19DA1EE82484009F6AEE /* ExpiryTests.swift in Sources */,
865877
D59A19D81EE82484009F6AEE /* CapsuleTests.swift in Sources */,
@@ -885,6 +897,7 @@
885897
D59A19CF1EE82482009F6AEE /* DataCacheTests.swift in Sources */,
886898
D59A19D11EE82482009F6AEE /* JSONCacheTests.swift in Sources */,
887899
D59A19D31EE82482009F6AEE /* StringCacheTests.swift in Sources */,
900+
D5C24E261EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
888901
D59A19D71EE82482009F6AEE /* MemoryStorageTests.swift in Sources */,
889902
D59A19CD1EE82482009F6AEE /* ExpiryTests.swift in Sources */,
890903
D5C24DDA1EE8C29D00D2CF22 /* HybridCacheTests.swift in Sources */,
@@ -907,6 +920,7 @@
907920
buildActionMask = 2147483647;
908921
files = (
909922
D5291D941C283CFB00B702C9 /* MemoryStorage.swift in Sources */,
923+
D5C24E231EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
910924
D5291D8B1C283CFB00B702C9 /* Expiry.swift in Sources */,
911925
D5291D891C283CFB00B702C9 /* Cachable.swift in Sources */,
912926
D5291D8C1C283CFB00B702C9 /* JSON.swift in Sources */,
@@ -937,6 +951,7 @@
937951
D5C24DDE1EE8C45900D2CF22 /* HybridCacheTests.swift in Sources */,
938952
D5C24DF01EE8C51500D2CF22 /* DiskStorageTests.swift in Sources */,
939953
D5C24DE11EE8C49800D2CF22 /* TestHelper.swift in Sources */,
954+
D5C24E271EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
940955
D5C24DE81EE8C50200D2CF22 /* CodingTests.swift in Sources */,
941956
D59A19EA1EE824B4009F6AEE /* NSImageCacheTests.swift in Sources */,
942957
D5C24DEE1EE8C50B00D2CF22 /* NSDictionaryCacheTests.swift in Sources */,
@@ -959,6 +974,7 @@
959974
buildActionMask = 2147483647;
960975
files = (
961976
D5291C2E1C28220B00B702C9 /* Cachable.swift in Sources */,
977+
D5C24E221EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
962978
D5291C311C28220B00B702C9 /* JSON.swift in Sources */,
963979
D5291C361C28220B00B702C9 /* String+Cache.swift in Sources */,
964980
D5291C341C28220B00B702C9 /* Data+Cache.swift in Sources */,

README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* [Optional bonuses](#optional-bonuses)
2323
* [JSON](#json)
2424
* [Coding](#coding)
25+
* [CacheArray](#cachearray)
2526
* [What about images?](#what-about-images)
2627
* [Installation](#installation)
2728
* [Author](#author)
@@ -54,6 +55,7 @@ types.
5455
implemented for `UIImage`, `String`, `JSON` and `Data`.
5556
- [x] Error handling and logs.
5657
- [x] `Coding` protocol brings power of `NSCoding` to Swift structs and enums
58+
- [x] `CacheArray` allows to cache an array of `Cachable` objects.
5759
- [x] Extensive unit test coverage and great documentation.
5860
- [x] iOS, tvOS and macOS support.
5961

@@ -185,6 +187,8 @@ cache["key"] = nil
185187
print(cache["key"]) // Prints nil
186188
```
187189

190+
Note that default cache expiry will be used when you use subscript.
191+
188192
**Sync API**
189193

190194
```swift
@@ -195,10 +199,10 @@ let cache = SpecializedCache<UIImage>(name: "ImageCache")
195199
try cache.addObject(UIImage(named: "image.png"), forKey: "image")
196200

197201
// Get object from cache
198-
let image: UIImage? = cache.object(forKey: "image")
202+
let image = cache.object(forKey: "image")
199203

200204
// Get object with expiry date
201-
let entry: CacheEntry<String>? = cache.cacheEntry(forKey: "image")
205+
let entry = cache.cacheEntry(forKey: "image")
202206
print(entry?.object)
203207
print(entry?.expiry.date) // Prints expiry date
204208

@@ -366,6 +370,29 @@ print(object?.title) // Prints title
366370

367371
```
368372

373+
### CacheArray
374+
375+
You can use `CacheArray` to cache an array of `Cachable` objects.
376+
377+
```swift
378+
// SpecializedCache
379+
let cache = SpecializedCache<CacheArray<String>>(name: "User")
380+
let object = CacheArray(elements: ["string1", "string2"])
381+
try cache.addObject(object, forKey: "array")
382+
let array = cache.object(forKey: "array")?.elements
383+
print(array) // Prints ["string1", "string2"]
384+
```
385+
386+
```swift
387+
// HybridCache
388+
let cache = HybridCache(name: "Mix")
389+
let object = CacheArray(elements: ["string1", "string2"])
390+
try cache.addObject(object, forKey: "array")
391+
let array = (cache.object(forKey: "array") as CacheArray<String>?)?.elements
392+
print(array) // Prints ["string1", "string2"]
393+
```
394+
395+
369396
## What about images?
370397

371398
As being said before, `Cache` works with any kind of `Cachable` types, with no
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Foundation
2+
3+
/// A wrapper around array of `Cachable` objects that performs data decoding and encoding.
4+
public struct CacheArray<T: Cachable>: Cachable {
5+
/// Array of elements
6+
public let elements: [T]
7+
8+
/**
9+
Creates an instance of `CacheArray`
10+
- Parameter elements: Array of `Cachable` elements
11+
*/
12+
public init(elements: [T]) {
13+
self.elements = elements
14+
}
15+
16+
/**
17+
Creates an instance from Data.
18+
- Parameter data: Data to decode from
19+
- Returns: An optional CacheType
20+
*/
21+
public static func decode(_ data: Data) -> CacheArray<T>? {
22+
// Unarchive object as an array of data
23+
guard let dataArray = (NSKeyedUnarchiver.unarchiveObject(with: data) as? NSArray) as? [Data] else {
24+
return nil
25+
}
26+
27+
do {
28+
// Decode data to element of `T` type.
29+
let elements = try dataArray.map ({ data -> T in
30+
guard let element = T.decode(data) as? T else {
31+
throw Error.decodingFailed
32+
}
33+
return element
34+
})
35+
return CacheArray(elements: elements)
36+
} catch {
37+
return nil
38+
}
39+
}
40+
41+
/**
42+
Encodes an instance to Data.
43+
- Returns: Optional Data
44+
*/
45+
public func encode() -> Data? {
46+
do {
47+
// Create an array of data to be able to archive as `NSArray`
48+
let dataArray = try elements.map ({ element -> Data in
49+
guard let data = element.encode() else {
50+
throw Error.encodingFailed
51+
}
52+
return data
53+
})
54+
return NSKeyedArchiver.archivedData(withRootObject: NSArray(array: dataArray))
55+
} catch {
56+
return nil
57+
}
58+
}
59+
}
60+
61+
private enum Error: Swift.Error {
62+
case encodingFailed
63+
case decodingFailed
64+
}

Tests/iOS/Tests/Cache/HybridCacheTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final class HybridCacheTests: XCTestCase {
3131

3232
// MARK: - Async caching
3333

34-
func testAsyncAddObject() {
34+
func testAsyncAddObject() throws {
3535
let expectation1 = self.expectation(description: "Save Expectation")
3636
let expectation2 = self.expectation(description: "Save To Memory Expectation")
3737
let expectation3 = self.expectation(description: "Save To Disk Expectation")
@@ -92,10 +92,10 @@ final class HybridCacheTests: XCTestCase {
9292
}
9393

9494
/// Should resolve from disk and set in-memory cache if object not in-memory
95-
func testAsyncObjectCopyToMemory() {
95+
func testAsyncObjectCopyToMemory() throws {
9696
let expectation = self.expectation(description: "Expectation")
9797

98-
try! cache.manager.backStorage.addObject(object, forKey: key)
98+
try cache.manager.backStorage.addObject(object, forKey: key)
9999
cache.async.object(forKey: key) { (cachedObject: String?) in
100100
XCTAssertNotNil(cachedObject)
101101
XCTAssertEqual(cachedObject, self.object)

Tests/iOS/Tests/Cache/SpecializedCacheTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final class SpecializedCacheTests: XCTestCase {
3131

3232
// MARK: - Async caching
3333

34-
func testAsyncAddObject() {
34+
func testAsyncAddObject() throws {
3535
let expectation1 = self.expectation(description: "Save Expectation")
3636
let expectation2 = self.expectation(description: "Save To Memory Expectation")
3737
let expectation3 = self.expectation(description: "Save To Disk Expectation")
@@ -94,10 +94,10 @@ final class SpecializedCacheTests: XCTestCase {
9494
}
9595

9696
/// Should resolve from disk and set in-memory cache if object not in-memory
97-
func testAsyncObjectCopyToMemory() {
97+
func testAsyncObjectCopyToMemory() throws {
9898
let expectation = self.expectation(description: "Expectation")
9999

100-
try! cache.manager.backStorage.addObject(object, forKey: key)
100+
try cache.manager.backStorage.addObject(object, forKey: key)
101101
cache.async.object(forKey: key) { cachedObject in
102102
XCTAssertNotNil(cachedObject)
103103
XCTAssertEqual(cachedObject?.firstName, self.object.firstName)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import XCTest
2+
@testable import Cache
3+
4+
// MARK: - Test case
5+
6+
final class CacheArrayTests: XCTestCase {
7+
private let fileManager = FileManager()
8+
9+
/// Test encoding and decoding
10+
func testDiskStorageWithStringArray() throws {
11+
let storage = DiskStorage(name: "Storage")
12+
let array: [String] = ["Test1", "Test2"]
13+
14+
try storage.addObject(CacheArray(elements: array), forKey: "key")
15+
let cachedObject: CacheArray<String>? = try storage.object(forKey: "key")
16+
17+
XCTAssertEqual(cachedObject?.elements.count, 2)
18+
XCTAssertEqual(cachedObject?.elements[0], "Test1")
19+
XCTAssertEqual(cachedObject?.elements[1], "Test2")
20+
21+
// Cleanup
22+
try fileManager.removeItem(atPath: storage.path)
23+
}
24+
25+
/// Test encoding and decoding
26+
func testDiskStorageWithCodingArray() throws {
27+
let storage = DiskStorage(name: "Storage")
28+
let array: [User] = [
29+
User(firstName: "First1", lastName: "Last1"),
30+
User(firstName: "First2", lastName: "Last2")
31+
]
32+
33+
try storage.addObject(CacheArray(elements: array), forKey: "key")
34+
let cachedObject: CacheArray<User>? = try storage.object(forKey: "key")
35+
36+
XCTAssertEqual(cachedObject?.elements.count, 2)
37+
XCTAssertEqual(cachedObject?.elements[0].firstName, "First1")
38+
XCTAssertEqual(cachedObject?.elements[0].lastName, "Last1")
39+
XCTAssertEqual(cachedObject?.elements[1].firstName, "First2")
40+
XCTAssertEqual(cachedObject?.elements[1].lastName, "Last2")
41+
42+
// Cleanup
43+
try fileManager.removeItem(atPath: storage.path)
44+
}
45+
46+
/// Test encoding and decoding
47+
func testSpecializedCacheWithCodingArray() throws {
48+
let cache = SpecializedCache<CacheArray<User>>(name: "Cache")
49+
let array: [User] = [
50+
User(firstName: "First1", lastName: "Last1"),
51+
User(firstName: "First2", lastName: "Last2")
52+
]
53+
54+
cache["key"] = CacheArray(elements: array)
55+
let cachedObject = cache["key"]
56+
57+
XCTAssertEqual(cachedObject?.elements.count, 2)
58+
XCTAssertEqual(cachedObject?.elements[0].firstName, "First1")
59+
XCTAssertEqual(cachedObject?.elements[0].lastName, "Last1")
60+
XCTAssertEqual(cachedObject?.elements[1].firstName, "First2")
61+
XCTAssertEqual(cachedObject?.elements[1].lastName, "Last2")
62+
63+
// Cleanup
64+
try cache.clear()
65+
}
66+
}

Tests/iOS/Tests/DataStructures/CodingTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ final class CodingTests: XCTestCase {
2121
}
2222

2323
/// Test encoding and decoding
24-
func testCoding() {
25-
try! storage.addObject(object, forKey: key)
26-
let cachedObject: User? = try! storage.object(forKey: key)
24+
func testCoding() throws {
25+
try storage.addObject(object, forKey: key)
26+
let cachedObject: User? = try storage.object(forKey: key)
2727
XCTAssertEqual(cachedObject?.firstName, "First")
2828
XCTAssertEqual(cachedObject?.lastName, "Last")
2929
}

Tests/iOS/Tests/DataStructures/ExpiryTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class ExpiryTests: XCTestCase {
1818
XCTAssertEqualWithAccuracy(
1919
expiry.date.timeIntervalSinceReferenceDate,
2020
date.timeIntervalSinceReferenceDate,
21-
accuracy: 0.01
21+
accuracy: 0.1
2222
)
2323
}
2424

Tests/iOS/Tests/Extensions/JSONCacheTests.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import XCTest
33

44
final class JSONCacheTests: XCTestCase {
55
/// Test that it decodes a dictionary from NSData
6-
func testDecodeWithDictionary() {
6+
func testDecodeWithDictionary() throws {
77
let object = ["key": "value"]
8-
let data = try! JSONSerialization.data(
8+
let data = try JSONSerialization.data(
99
withJSONObject: object,
1010
options: JSONSerialization.WritingOptions()
1111
)
@@ -21,9 +21,9 @@ final class JSONCacheTests: XCTestCase {
2121
}
2222

2323
/// Test that it decodes an array from NSData
24-
func testDecodeWithArray() {
24+
func testDecodeWithArray() throws {
2525
let object = ["value1", "value2", "value3"]
26-
let data = try! JSONSerialization.data(
26+
let data = try JSONSerialization.data(
2727
withJSONObject: object,
2828
options: JSONSerialization.WritingOptions()
2929
)
@@ -40,9 +40,9 @@ final class JSONCacheTests: XCTestCase {
4040
}
4141

4242
/// Test that it encodes a dictionary to NSData
43-
func testEncodeWithDictionary() {
43+
func testEncodeWithDictionary() throws {
4444
let object = ["key": "value"]
45-
let data = try! JSONSerialization.data(
45+
let data = try JSONSerialization.data(
4646
withJSONObject: object,
4747
options: JSONSerialization.WritingOptions()
4848
)
@@ -52,9 +52,9 @@ final class JSONCacheTests: XCTestCase {
5252
}
5353

5454
/// Test that it encodes an array to NSData
55-
func testEncodeWithArray() {
55+
func testEncodeWithArray() throws {
5656
let object = ["value1", "value2", "value3"]
57-
let data = try! JSONSerialization.data(
57+
let data = try JSONSerialization.data(
5858
withJSONObject: object,
5959
options: JSONSerialization.WritingOptions()
6060
)

0 commit comments

Comments
 (0)