RxSwiftでUserDefaults


RxSwiftでUserDefaultsをControlPropertyとして扱う

TextField.rx.textのように双方向に対応してます。(ObserverでありObservableでもあるみたいな。)
意外とCocoaPodsで転がってそうなコードなのになかった。

UserDefaults+Rx.swift

import Foundation
import RxSwift
import RxCocoa

public extension Reactive where Base: UserDefaults {

    func `default`<E: Equatable>(forKey key: String) -> ControlProperty<E?> {
        let source = Observable.deferred { [weak defaults = self.base as UserDefaults] () -> Observable<E?> in
            let center = NotificationCenter.default
            let initial = defaults?.object(forKey: key) as? E
            let changes = center.rx.notification(UserDefaults.didChangeNotification)
                .map { _ in defaults?.object(forKey: key) as? E }

            return Observable.just(initial)
                .concat(changes)
                .distinctUntilChanged { previous, next in
                    guard let previous = previous, let next = next else { return false }
                    return previous == next
            }
        }

        let binder = Binder(self.base) { (defaults, value: E?) in
            defaults.set(value, forKey: key)
        }

        return ControlProperty(values: source, valueSink: binder)
    }

    func url(forKey key: String) -> ControlProperty<URL?> {
        let source = Observable.deferred { [weak defaults = self.base as UserDefaults] () -> Observable<URL?> in
            let center = NotificationCenter.default
            let initial = defaults?.url(forKey: key)
            let changes = center.rx.notification(UserDefaults.didChangeNotification)
                .map { _ in defaults?.url(forKey: key) }

            return Observable.just(initial)
                .concat(changes)
                .distinctUntilChanged { previous, next in
                    guard let previous = previous, let next = next else { return false }
                    return previous == next
            }
        }

        let binder = Binder(self.base) { (defaults, value: URL?) in
            defaults.set(value, forKey: key)
        }

        return ControlProperty(values: source, valueSink: binder)
    }
}

使用例

いろいろRx系ライブラリ使って書いてるんでなんとなく雰囲気だけ感じてください。
ドキュメントピッカー開いて選ばれたやつをUserDefaultsに記憶。

let documentUrl = UserDefaults.standard.rx.url(forKey: "documentUrl")

pickDocumentButton.rx.tap
    .mapTo(UIDocumentPickerViewController(documentTypes: ["public.item"], in: .open))
    .do(onNext: presentView.accept)
    .flatMapAt(\.rx.didPickDocumentAt)
    .bind(to: documentUrl)
    .disposed(by: disposeBag)