LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-08-21

既出かもしれないが備忘録として。
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()
            }
        }
    }
}

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1