既出かもしれないが備忘録として。
EmbeddedFrameworkにしてかってます。
Objectのネストは考えていないです。
データ自体はJesonで保存されます。
Storage
Storage.swift
public class Storage {
public static let shared = Storage()
public func store<T: Object>(object: T) throws {
let ud = UserDefaults.standard
let encorder = JSONEncoder()
let data = try encorder.encode(object)
ud.set(data, forKey: T.key)
ud.synchronize()
}
public func find<T: Object>(objectType: T.Type) throws -> T? {
let ud = UserDefaults.standard
let decorder = JSONDecoder()
if let data = ud.data(forKey: T.key) {
let object = try decorder.decode(T.self, from: data)
return object
}
return nil
}
public func remove<T: Object>(objectType: T.Type) {
let ud = UserDefaults.standard
ud.removeObject(forKey: T.key)
ud.synchronize()
}
}
Object
Object.swift
public protocol Object: Codable {
static var key: String { get }
}
public extension Object {
static var key: String {
return String(describing: type(of: self))
}
}
Rx用のExtension
Storage+Rx.swift
import RxSwift
extension Storage: ReactiveCompatible { }
public extension Reactive where Base: Storage {
public func store<T: Object>(object: T) -> Completable {
do {
try base.store(object: object)
return .empty()
} catch let e {
return .error(e)
}
}
public func find<T: Object>(objectType: T.Type) -> Maybe<T> {
let ud = UserDefaults.standard
let decorder = JSONDecoder()
if let data = ud.data(forKey: T.key) {
do {
let object = try decorder.decode(T.self, from: data)
return .just(object)
} catch let e {
return .error(e)
}
}
return .empty()
}
public func remove<T: Object>(objectType: T.Type) -> Completable {
let ud = UserDefaults.standard
ud.removeObject(forKey: T.key)
ud.synchronize()
return .empty()
}
public static func store<T: Object>(object: T) -> Completable {
return Base.shared.rx.store(object: object)
}
public static func find<T: Object>(objectType: T.Type) -> Maybe<T> {
return Base.shared.rx.find(objectType: objectType)
}
public static func remove<T: Object>(objectType: T.Type) -> Completable {
return Base.shared.rx.remove(objectType: objectType)
}
}
Object+Rx.swift
import RxSwift
public extension Object {
public func store() -> Completable {
return Storage.rx.store(object: self)
}
public static func find() -> Maybe<Self> {
return Storage.rx.find(objectType: Self.self)
}
public static func remove() -> Completable {
return Storage.rx.remove(objectType: Self.self)
}
}
使い方兼簡単なTest
UserDefaultsStorageTests.swift
import XCTest
import RxSwift
import RxBlocking
@testable import UserDefaultsStorage
struct TestObject1: Object {
let id: String
let name: String
let age: Int
let createdAt: Date
}
struct TestObject2: Object {
let id: String
let name: String
let age: Int
let createdAt: Date
}
class UserDefaultsStorageTests: XCTestCase {
var disposeBag = DisposeBag()
override func setUp() {
super.setUp()
disposeBag = DisposeBag()
let appDomain = Bundle.main.bundleIdentifier ?? ""
UserDefaults.standard.removePersistentDomain(forName: appDomain)
}
func testRx保存検索上書き() {
for _ in 0...10 {
let data = TestObject1(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
data.store().subscribe().disposed(by: disposeBag)
do {
let result = try TestObject1.find().toBlocking().last()!
XCTAssertEqual(data.id, result.id)
XCTAssertEqual(data.name, result.name)
XCTAssertEqual(data.age, result.age)
XCTAssertEqual(data.createdAt, result.createdAt)
} catch {
XCTFail()
}
}
}
func testRx保存検索削除() {
for _ in 0...10 {
let data = TestObject1(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
data.store().subscribe().disposed(by: disposeBag)
do {
if let result = try TestObject1.find().toBlocking().last() {
XCTAssertEqual(data.id, result.id)
XCTAssertEqual(data.name, result.name)
XCTAssertEqual(data.age, result.age)
XCTAssertEqual(data.createdAt, result.createdAt)
} else {
XCTFail()
}
} catch {
XCTFail()
}
TestObject1.remove().subscribe().disposed(by: disposeBag)
do {
if let _ = try TestObject1.find().toBlocking().last() {
XCTFail()
}
} catch {
XCTFail()
}
}
}
func testRx複数データ保存検索上書き() {
for _ in 0...10 {
let data1 = TestObject1(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
let data2 = TestObject2(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
data1.store().subscribe().disposed(by: disposeBag)
data2.store().subscribe().disposed(by: disposeBag)
do {
let result1 = try TestObject1.find().toBlocking().last()!
let result2 = try TestObject2.find().toBlocking().last()!
XCTAssertEqual(data1.id, result1.id)
XCTAssertEqual(data1.name, result1.name)
XCTAssertEqual(data1.age, result1.age)
XCTAssertEqual(data1.createdAt, result1.createdAt)
XCTAssertEqual(data2.id, result2.id)
XCTAssertEqual(data2.name, result2.name)
XCTAssertEqual(data2.age, result2.age)
XCTAssertEqual(data2.createdAt, result2.createdAt)
} catch {
XCTFail()
}
}
}
func testRx複数データ保存検索削除() {
for _ in 0...10 {
let data1 = TestObject1(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
let data2 = TestObject2(
id: ValueHelper.shared.randomString(),
name: ValueHelper.shared.randomString(),
age: ValueHelper.shared.randomInt(),
createdAt: Date()
)
data1.store().subscribe().disposed(by: disposeBag)
data2.store().subscribe().disposed(by: disposeBag)
do {
let result1 = try TestObject1.find().toBlocking().last()!
let result2 = try TestObject2.find().toBlocking().last()!
XCTAssertEqual(data1.id, result1.id)
XCTAssertEqual(data1.name, result1.name)
XCTAssertEqual(data1.age, result1.age)
XCTAssertEqual(data1.createdAt, result1.createdAt)
XCTAssertEqual(data2.id, result2.id)
XCTAssertEqual(data2.name, result2.name)
XCTAssertEqual(data2.age, result2.age)
XCTAssertEqual(data2.createdAt, result2.createdAt)
} catch {
XCTFail()
}
TestObject1.remove().subscribe().disposed(by: disposeBag)
TestObject2.remove().subscribe().disposed(by: disposeBag)
do {
if let _ = try TestObject1.find().toBlocking().last() {
XCTFail()
}
if let _ = try TestObject2.find().toBlocking().last() {
XCTFail()
}
} catch {
XCTFail()
}
}
}
}