CodableでUserDefaultsを読み書きする仕組み作ってみた


既出かもしれないが備忘録として。
EmbeddedFrameworkにしてかってます。

Objectのネストは考えていないです。
データ自体はJesonで保存されます。

## Storage

```swift: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

```swift: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

```swift: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)
    }
    
}
```

```swift: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

```swift: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()
            }
        }
    }
}

```