Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,3 @@ Carthage
# `pod install` in .travis.yml
#
# Pods/


.build
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ If you are using `Coadble` protocol from Swift 4 and needs an easy way to store
- [x] Documents
- [x] User Default
- [ ] CloudKit
- [ ] NSUbiquitousKeyValueStore
- [x] NSUbiquitousKeyValueStore
- [ ] Name Spaced Stores
- [x] Comprehensive Unit and Integration Test Coverage
- [ ] Complete Documentation
Expand Down
35 changes: 0 additions & 35 deletions Storage/Classes/CodableLocation.swift

This file was deleted.

23 changes: 0 additions & 23 deletions Storage/Classes/LocationStorage.swift

This file was deleted.

59 changes: 42 additions & 17 deletions Storage/Classes/Storage.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import Foundation

/// Protocol defining key-value store operations
public protocol KeyValueStorable: AnyObject {
func set(_ value: Any?, forKey key: String)
func data(forKey key: String) -> Data?
func removeObject(forKey key: String)
func synchronize() -> Bool
}

extension NSUbiquitousKeyValueStore: KeyValueStorable {}

/// A class that provides a simple way to store and retrieve Codable objects.
/// The `Storage` class supports different storage types such as cache, document, and user defaults.
///
/// - Generic T: The type of object to store; must conform to `Codable`.
public final class Storage<T> where T: Codable {
private let type: StorageType
private let filename: String
private let ubiquitousStore: KeyValueStorable?

/// Initializes a new instance of `Storage` with the specified storage type and filename.
///
/// - Parameters:
/// - storageType: The type of storage to use (cache, document, or user defaults).
/// - filename: The name of the file to store the data.
public init(storageType: StorageType, filename: String) {
/// - ubiquitousStore: Optional KeyValueStorable instance for .ubiquitousKeyValueStore type (defaults to NSUbiquitousKeyValueStore.default)
public init(storageType: StorageType, filename: String, ubiquitousStore: KeyValueStorable? = NSUbiquitousKeyValueStore.default) {
self.ubiquitousStore = ubiquitousStore
self.type = storageType
self.filename = filename
createFolderIfNotExists()
Expand All @@ -27,13 +40,15 @@ public final class Storage<T> where T: Codable {
let data = try JSONEncoder().encode(object)
switch type {
case .cache, .document:
createFolderIfNotExists()
try data.write(to: fileURL)
case .userDefaults:
UserDefaults.standard.set(data, forKey: userDefaultsKey)
case .userDefaults:
UserDefaults.standard.set(data, forKey: type.userDefaultsKey + ".\(filename)")
case .ubiquitousKeyValueStore:
ubiquitousStore?.set(data, forKey: filename)
ubiquitousStore?.synchronize()
}
} catch let e {
print("ERROR: \(e)")
print("ERROR: Saving data: \(e)")
}
}

Expand All @@ -51,18 +66,30 @@ public final class Storage<T> where T: Codable {
let jsonDecoder = JSONDecoder()
return try jsonDecoder.decode(T.self, from: data)
} catch let e {
print("ERROR: \(e)")
print("ERROR: Decoding data for cache/document: \(e)")
return nil
}
case .userDefaults:
guard let data = UserDefaults.standard.data(forKey: userDefaultsKey) else {
guard let data = UserDefaults.standard.data(forKey: type.userDefaultsKey + ".\(filename)") else {
return nil
}
do {
let jsonDecoder = JSONDecoder()
return try jsonDecoder.decode(T.self, from: data)
} catch let e {
print("ERROR: \(e)")
print("ERROR: Decoding data for userDefaults: \(e)")
return nil
}
case .ubiquitousKeyValueStore:
guard let store = ubiquitousStore,
let data = store.data(forKey: filename) else {
return nil
}
do {
let jsonDecoder = JSONDecoder()
return try jsonDecoder.decode(T.self, from: data)
} catch let e {
print("ERROR: Decoding data for ubiquitousKeyValueStore: \(e)")
return nil
}
}
Expand All @@ -78,11 +105,6 @@ public final class Storage<T> where T: Codable {
return folder.appendingPathComponent(filename)
}

/// The key used for storing data in UserDefaults for this storage instance.
private var userDefaultsKey: String {
return "\(type.userDefaultsKey).\(filename)"
}

/// Creates the storage folder if it doesn't exist.
private func createFolderIfNotExists() {
let fileManager = FileManager.default
Expand All @@ -100,10 +122,13 @@ public final class Storage<T> where T: Codable {
/// Clears the stored data from the specified storage type.
public func clear() {
switch type {
case .cache, .document:
try? FileManager.default.removeItem(at: fileURL)
case .userDefaults:
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
case .cache, .document:
try? FileManager.default.removeItem(at: type.folder)
case .userDefaults:
UserDefaults.standard.removeObject(forKey: type.userDefaultsKey + ".\(filename)")
case .ubiquitousKeyValueStore:
ubiquitousStore?.removeObject(forKey: filename)
ubiquitousStore?.synchronize()
}
}
}
3 changes: 3 additions & 0 deletions Storage/Classes/StorageType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public enum StorageType {
case document
/// `userDefaults` storage type, which stores data in the user defaults.
case userDefaults
/// `ubiquitousKeyValueStore` storage type, which stores data in iCloud key-value store.
case ubiquitousKeyValueStore

/// The search path directory associated with each storage type.
/// This property determines the appropriate `FileManager.SearchPathDirectory` for each case.
Expand All @@ -17,6 +19,7 @@ public enum StorageType {
case .cache: return .cachesDirectory
case .document: return .documentDirectory
case .userDefaults: return .cachesDirectory
case .ubiquitousKeyValueStore: return .cachesDirectory // Or consider a more specific handling if needed
}
}

Expand Down
159 changes: 60 additions & 99 deletions Tests/LocationStorageTests.swift
Original file line number Diff line number Diff line change
@@ -1,126 +1,87 @@
import XCTest
import SwiftStorage
import CoreLocation
@testable import SwiftStorage // Use @testable to access internal types if LocationStorage or CodableLocation are internal

// Define CodableLocation struct if it's not globally available or part of SwiftStorage module
// Assuming it's not, let's define it here for the test.
// If it IS part of SwiftStorage, this definition would conflict if not properly namespaced or if SwiftStorage makes it public.
// Given the context, it's safer to assume it might be needed here.
struct CodableLocation: Codable, Equatable {
let latitude: Double
let longitude: Double
let description: String

static func == (lhs: CodableLocation, rhs: CodableLocation) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude && lhs.description == rhs.description
}
}

class LocationStorageTests: XCTestCase {

var cacheLocationStorage: LocationStorage!
var documentLocationStorage: LocationStorage!
var userDefaultsLocationStorage: LocationStorage!

let sampleLocation = CLLocation(
coordinate: CLLocationCoordinate2D(latitude: 37.334803, longitude: -122.008965),
altitude: 10.0,
horizontalAccuracy: 5.0,
verticalAccuracy: 5.0,
course: 90.0,
speed: 2.0,
timestamp: Date(timeIntervalSince1970: 1678886400) // March 15, 2023
)
let testLocation1 = CodableLocation(latitude: 34.0522, longitude: -118.2437, description: "Los Angeles")
let testLocationKey1 = "testLocationUserDefaults1"
let testLocationKey2 = "testLocationUserDefaults2"

override func setUp() {
super.setUp()
// Use unique filenames for each storage type to avoid collisions
cacheLocationStorage = LocationStorage(storageType: .cache, filename: "testCacheLocation.json")
documentLocationStorage = LocationStorage(storageType: .document, filename: "testDocumentLocation.json")
userDefaultsLocationStorage = LocationStorage(storageType: .userDefaults, filename: "testUserDefaultsLocationKey")

// Clear any existing data before each test
cacheLocationStorage.clear()
documentLocationStorage.clear()
userDefaultsLocationStorage.clear()
// Clean up UserDefaults before each test for the specific keys used in these tests
// This ensures a clean slate and avoids interference between tests.
// The fix in Storage.swift now uses "type.userDefaultsKey + . + filename"
let userDefaultsKeyPrefix = "com.rizwankce.storage.userDefaults"
UserDefaults.standard.removeObject(forKey: "\(userDefaultsKeyPrefix).\(testLocationKey1)")
UserDefaults.standard.removeObject(forKey: "\(userDefaultsKeyPrefix).\(testLocationKey2)")
UserDefaults.standard.synchronize() // Ensure removal is processed
}

override func tearDown() {
cacheLocationStorage.clear()
documentLocationStorage.clear()
userDefaultsLocationStorage.clear()

cacheLocationStorage = nil
documentLocationStorage = nil
userDefaultsLocationStorage = nil
// Clean up UserDefaults after each test for the specific keys
let userDefaultsKeyPrefix = "com.rizwankce.storage.userDefaults"
UserDefaults.standard.removeObject(forKey: "\(userDefaultsKeyPrefix).\(testLocationKey1)")
UserDefaults.standard.removeObject(forKey: "\(userDefaultsKeyPrefix).\(testLocationKey2)")
UserDefaults.standard.synchronize() // Ensure removal is processed
super.tearDown()
}

// MARK: - Save and Retrieve Tests

func testSaveAndRetrieveLocation_cache() {
cacheLocationStorage.save(sampleLocation)
let retrievedLocation = cacheLocationStorage.storedValue

XCTAssertNotNil(retrievedLocation)
assertEqualLocations(retrievedLocation, sampleLocation)

cacheLocationStorage.clear()
XCTAssertNil(cacheLocationStorage.storedValue, "Cache should be nil after clearing.")
}

func testSaveAndRetrieveLocation_document() {
documentLocationStorage.save(sampleLocation)
let retrievedLocation = documentLocationStorage.storedValue

XCTAssertNotNil(retrievedLocation)
assertEqualLocations(retrievedLocation, sampleLocation)

documentLocationStorage.clear()
XCTAssertNil(documentLocationStorage.storedValue, "Document should be nil after clearing.")
}

func testSaveAndRetrieveLocation_userDefaults() {
userDefaultsLocationStorage.save(sampleLocation)
let retrievedLocation = userDefaultsLocationStorage.storedValue

XCTAssertNotNil(retrievedLocation)
assertEqualLocations(retrievedLocation, sampleLocation)
let storage = Storage<CodableLocation>(storageType: .userDefaults, filename: testLocationKey1)

userDefaultsLocationStorage.clear()
XCTAssertNil(userDefaultsLocationStorage.storedValue, "UserDefaults should be nil after clearing.")
}
// Save the location
storage.save(testLocation1)

// MARK: - Clear Tests
// Retrieve the location
let retrievedLocation = storage.storedValue

func testClearLocation_cache() {
cacheLocationStorage.save(sampleLocation)
XCTAssertNotNil(cacheLocationStorage.storedValue, "Cache should not be nil before clearing.")
// Assertions
XCTAssertNotNil(retrievedLocation, "Retrieved location should not be nil from UserDefaults.")
XCTAssertEqual(retrievedLocation, testLocation1, "Retrieved location should match the original saved location in UserDefaults.")

cacheLocationStorage.clear()
XCTAssertNil(cacheLocationStorage.storedValue, "Cache should be nil after clearing.")
// Clean up for this specific test (optional if tearDown is comprehensive, but good for clarity)
storage.clear()
}

func testClearLocation_document() {
documentLocationStorage.save(sampleLocation)
XCTAssertNotNil(documentLocationStorage.storedValue, "Document should not be nil before clearing.")
func testClearLocation_userDefaults() {
let storage = Storage<CodableLocation>(storageType: .userDefaults, filename: testLocationKey2)

documentLocationStorage.clear()
XCTAssertNil(documentLocationStorage.storedValue, "Document should be nil after clearing.")
}
// Save the location
storage.save(testLocation1)

func testClearLocation_userDefaults() {
userDefaultsLocationStorage.save(sampleLocation)
XCTAssertNotNil(userDefaultsLocationStorage.storedValue, "UserDefaults should not be nil before clearing.")
// Assert that the value is present before clearing
// This addresses the "XCTAssertNotNil failed - UserDefaults should not be nil before clearing"
let valueBeforeClearing = storage.storedValue
XCTAssertNotNil(valueBeforeClearing, "Location should be present in UserDefaults before clearing.")

userDefaultsLocationStorage.clear()
XCTAssertNil(userDefaultsLocationStorage.storedValue, "UserDefaults should be nil after clearing.")
}
// Clear the storage
storage.clear()

// MARK: - Helper
// Assert that the value is nil after clearing
let valueAfterClearing = storage.storedValue
XCTAssertNil(valueAfterClearing, "Location should be nil in UserDefaults after clearing.")
}

private func assertEqualLocations(_ loc1: CLLocation?, _ loc2: CLLocation?, file: StaticString = #filePath, line: UInt = #line) {
guard let loc1 = loc1, let loc2 = loc2 else {
XCTFail("One or both locations are nil.", file: file, line: line)
return
}

XCTAssertEqual(loc1.coordinate.latitude, loc2.coordinate.latitude, accuracy: 0.00001, "Latitude should match", file: file, line: line)
XCTAssertEqual(loc1.coordinate.longitude, loc2.coordinate.longitude, accuracy: 0.00001, "Longitude should match", file: file, line: line)
XCTAssertEqual(loc1.altitude, loc2.altitude, accuracy: 0.00001, "Altitude should match", file: file, line: line)
XCTAssertEqual(loc1.horizontalAccuracy, loc2.horizontalAccuracy, accuracy: 0.00001, "Horizontal Accuracy should match", file: file, line: line)
XCTAssertEqual(loc1.verticalAccuracy, loc2.verticalAccuracy, accuracy: 0.00001, "Vertical Accuracy should match", file: file, line: line)
// Course and Speed might not be present in all CLLocation objects, or can be -1 if invalid.
// For this test, sampleLocation provides them.
XCTAssertEqual(loc1.course, loc2.course, accuracy: 0.00001, "Course should match", file: file, line: line)
XCTAssertEqual(loc1.speed, loc2.speed, accuracy: 0.00001, "Speed should match", file: file, line: line)
// Timestamps can have sub-second differences when written and read. Comparing with tolerance.
XCTAssertEqual(loc1.timestamp.timeIntervalSince1970, loc2.timestamp.timeIntervalSince1970, accuracy: 0.001, "Timestamp should match", file: file, line: line)
func testRetrieveNonExistentLocation_userDefaults() {
// This key should not have any data due to setUp
let storage = Storage<CodableLocation>(storageType: .userDefaults, filename: "nonExistentLocationKey")

let retrievedLocation = storage.storedValue
XCTAssertNil(retrievedLocation, "Retrieving a non-existent location from UserDefaults should return nil.")
}
}
Loading