Skip to content

Instantly share code, notes, and snippets.

@ryotapoi
Last active July 18, 2020 13:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryotapoi/654f6330766be3db8666ba71ad1a21a8 to your computer and use it in GitHub Desktop.
Save ryotapoi/654f6330766be3db8666ba71ad1a21a8 to your computer and use it in GitHub Desktop.
import UIKit
import Combine
// MARK: - UserDefaults
public protocol UserDefaultsProtocol: NSObject {
func value<Value: UserDefaultCompatible>(type: Value.Type, forKey key: String, default defaultValue: Value) -> Value
func setValue<Value: UserDefaultCompatible>(_ value: Value, forKey key: String)
}
extension UserDefaults: UserDefaultsProtocol {
public func value<Value: UserDefaultCompatible>(type: Value.Type = Value.self, forKey key: String, default defaultValue: Value) -> Value {
guard let object = object(forKey: key) else { return defaultValue }
return Value(userDefaultObject: object) ?? defaultValue
}
public func setValue<Value: UserDefaultCompatible>(_ value: Value, forKey key: String) {
set(value.toUserDefaultObject(), forKey: key)
}
}
// MARK: - UserDefaultCompatible
public protocol UserDefaultCompatible {
init?(userDefaultObject: Any)
func toUserDefaultObject() -> Any?
}
extension UserDefaultCompatible where Self: Codable {
public init?(userDefaultObject: Any) {
guard let data = userDefaultObject as? Data else { return nil }
do {
self = try JSONDecoder().decode(Self.self, from: data)
} catch {
return nil
}
}
public func toUserDefaultObject() -> Any? {
try? JSONEncoder().encode(self)
}
}
extension UserDefaultCompatible where Self: NSObject, Self: NSCoding {
public init?(userDefaultObject: Any) {
guard let data = userDefaultObject as? Data else { return nil }
if let value = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Self {
self = value
} else {
return nil
}
}
public func toUserDefaultObject() -> Any? {
if let object = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) {
return object
} else {
return nil
}
}
}
extension Array: UserDefaultCompatible where Element: UserDefaultCompatible {
private struct UserDefaultCompatibleError: Error {}
public init?(userDefaultObject: Any) {
guard let objects = userDefaultObject as? [Any] else { return nil }
do {
let values = try objects.map { (object: Any) -> Element in
if let element = Element(userDefaultObject: object) {
return element
} else {
throw UserDefaultCompatibleError()
}
}
self = values
} catch {
return nil
}
}
public func toUserDefaultObject() -> Any? {
map { $0.toUserDefaultObject() }
}
}
extension Dictionary: UserDefaultCompatible where Key == String, Value: UserDefaultCompatible {
private struct UserDefaultCompatibleError: Swift.Error {}
public init?(userDefaultObject: Any) {
guard let objects = userDefaultObject as? [String: Any] else { return nil }
do {
let values = try objects.mapValues { object -> Value in
if let value = Value(userDefaultObject: object) {
return value
} else {
throw UserDefaultCompatibleError()
}
}
self = values
} catch {
return nil
}
}
public func toUserDefaultObject() -> Any? {
mapValues { $0.toUserDefaultObject() }
}
}
extension Optional: UserDefaultCompatible where Wrapped: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
self = Wrapped(userDefaultObject: userDefaultObject)
}
public func toUserDefaultObject() -> Any? {
flatMap { $0.toUserDefaultObject() }
}
}
extension Int: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension Double: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension Float: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension Bool: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension String: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension URL: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Data else { return nil }
guard let url = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(userDefaultObject) as? URL else { return nil }
self = url
}
public func toUserDefaultObject() -> Any? {
try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
}
}
extension Date: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
extension Data: UserDefaultCompatible {
public init?(userDefaultObject: Any) {
guard let userDefaultObject = userDefaultObject as? Self else { return nil }
self = userDefaultObject
}
public func toUserDefaultObject() -> Any? {
self
}
}
// Combine
extension UserDefaults {
public class Publisher<Output>: NSObject, Combine.Publisher where Output: UserDefaultCompatible, Output: Equatable {
public typealias Failure = Never // エラーは発生しないものとする
private let key: String // キー文字列
private let defaultValue: Output // キー文字列に対応する値がない場合の値
private let userDefaults: UserDefaultsProtocol // UserDefaultsの本体
private let subject: CurrentValueSubject<Output, Never> // CurrentValueSubjectに処理を移譲して簡単に実装する
public var value: Output {
get { subject.value }
set {
if newValue != subject.value {
subject.value = newValue
// 値が設定された時はUserDefaultsに保存する
userDefaults.setValue(newValue, forKey: key)
}
}
}
// テストなどでUserDefaultsに値を保存したくない時に備え、念のためUserDefaultsProtocolを使う
// ただテストを考慮しても、あまり必要ない気がしている
public init(key: String, default defaultValue: Output, userDefaults: UserDefaultsProtocol = UserDefaults.standard) {
self.key = key
self.defaultValue = defaultValue
self.userDefaults = userDefaults
self.subject = .init(userDefaults.value(type: Output.self, forKey: key, default: defaultValue))
super.init()
// KVOでUserDefaultsの監視開始
userDefaults.addObserver(self, forKeyPath: key, options: .new, context: nil)
}
deinit {
// KVO監視終了
userDefaults.removeObserver(self, forKeyPath: key)
}
// KVO監視で変更があったときに呼ばれる
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == key {
let newValue = change?[.newKey]
.flatMap(Output.init(userDefaultObject:))
?? defaultValue
if newValue != subject.value {
// UserDefaultsの更新通知から、UserDefaultsにさらに保存する必要はない
// `subject.value` に直接値を入れ、値の通知だけ行うようにする
subject.value = newValue
}
}
}
// Publisherの必須メソッド
// 何も考えずCurrentValueSubjectに処理を移譲する
public func receive<S>(
subscriber: S
) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input {
subject.receive(subscriber: subscriber)
}
}
}
// Property Wrapper
@propertyWrapper
public struct UserDefault<Value> where Value: UserDefaultCompatible, Value: Equatable {
private let publisher: UserDefaults.Publisher<Value>
// `@UserDefault("key", userDefaults: UserDefaults.standard) var a: Int = 0` の場合
// 最初の引数 `wrappedValue` にプロパティの初期値 `0` が渡される
// それ以降の引数は `@UserDefault` の引数が渡される
public init(wrappedValue defaultValue: Value, _ key: String, userDefaults: UserDefaultsProtocol = UserDefaults.standard) {
publisher = .init(key: key, default: defaultValue, userDefaults: userDefaults)
}
// プロパティの値の読み書き時はこれが呼ばれる
public var wrappedValue: Value {
get { publisher.value }
set { publisher.value = newValue }
}
// プロパティに$を付けた時はこれが呼ばれる
public var projectedValue: UserDefaults.Publisher<Value> { publisher }
}
// Test Code
struct User: Codable, Equatable, UserDefaultCompatible {
var name: String
}
class Record: NSObject, NSCoding, UserDefaultCompatible {
var name: String? = "abc"
init(name: String?) {
self.name = name
}
required init?(coder: NSCoder) {
name = coder.decodeObject(forKey: "name") as? String
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? Record else { return false }
return name == object.name
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(name)
return hasher.finalize()
}
}
struct Settings {
@UserDefault("user")
var user: User = .init(name: "abc")
@UserDefault("userOptional")
var userOptional: User? = nil
@UserDefault("users")
var users: [User] = []
@UserDefault("userArrayOptional")
var userArrayOptional: [User]? = nil
@UserDefault("userDictionary")
var userDictionary: [String: User] = [:]
@UserDefault("userDictionaryOptional")
var userDictionaryOptional: [String: User]? = nil
@UserDefault("int")
var int: Int = 5
@UserDefault("intOptional")
var intOptional: Int? = nil
@UserDefault("doubleValue")
var doubleValue: Double = 23.57
@UserDefault("doubleValueOptional")
var doubleValueOptional: Double? = nil
@UserDefault("floatValue")
var floatValue: Float = 1.23
@UserDefault("floatValueOptional")
var floatValueOptional: Float? = nil
@UserDefault("bool")
var bool: Bool = true
@UserDefault("boolOptional")
var boolOptional: Bool? = nil
@UserDefault("string")
var string: String = "defaultString"
@UserDefault("stringOptional")
var stringOptional: String? = nil
@UserDefault("url")
var url: URL = URL(string: "https://google.com")!
@UserDefault("urlOptional")
var urlOptional: URL? = nil
@UserDefault("date")
var date: Date = .init(timeIntervalSinceReferenceDate: 0)
@UserDefault("dateOptional")
var dateOptional: Date? = nil
@UserDefault("data")
var data: Data = .init()
@UserDefault("dataOptional")
var dataOptional: Data? = nil
@UserDefault("record")
var record: Record = .init(name: "default record name")
@UserDefault("recordOptional")
var recordOptional: Record? = nil
}
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
var settings = Settings()
settings.$user.sink { print($0) }
settings.user
settings.user = User(name: "change name")
settings.user
settings.$userOptional.sink { print($0) }
settings.userOptional
settings.userOptional = User(name: "optional user")
settings.userOptional
settings.userOptional = nil
settings.userOptional
settings.users
settings.users = [User(name: "change name")]
settings.users
settings.userArrayOptional
settings.userArrayOptional = [User(name: "change name")]
settings.userArrayOptional
settings.userDictionary
settings.userDictionary = ["key": User(name: "change name")]
settings.userDictionary
settings.userDictionaryOptional
settings.userDictionaryOptional = ["key": User(name: "change name")]
settings.userDictionaryOptional
settings.int
settings.int = 432
settings.int
settings.intOptional
settings.intOptional = 543
settings.intOptional
settings.doubleValue
settings.doubleValue = 5.68
settings.doubleValue
settings.doubleValueOptional
settings.doubleValueOptional = 111.3
settings.doubleValueOptional
settings.floatValue
settings.floatValue = 99.99
settings.floatValue
settings.floatValueOptional
settings.floatValueOptional = -32
settings.floatValueOptional
settings.bool
settings.bool = false
settings.bool
settings.boolOptional
settings.boolOptional = true
settings.boolOptional
settings.string
settings.string = "string"
settings.string
settings.stringOptional
settings.stringOptional = "optional string"
settings.stringOptional
settings.url
settings.url = URL(string: "https://yahoo.co.jp")!
settings.url
settings.urlOptional
settings.urlOptional = URL(string: "https://yahoo.co.jp")
settings.urlOptional
settings.date
settings.date = Date(timeIntervalSinceReferenceDate: 3600)
settings.date
settings.dateOptional
settings.dateOptional = Date(timeIntervalSinceReferenceDate: 7200)
settings.dateOptional
settings.data
settings.data = Data(base64Encoded: "1234")!
settings.data
settings.dataOptional
settings.dataOptional = Data(base64Encoded: "1234")!
settings.dataOptional
settings.record
settings.record = Record(name: "change name")
settings.record
settings.recordOptional
settings.recordOptional = Record(name: "change name")
settings.recordOptional
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
do {
let publisher1: UserDefaults.Publisher<Int> = .init(key: "key1", default: 1)
let publisher2: UserDefaults.Publisher<Int> = .init(key: "key2", default: 100)
publisher1.value // => 1
publisher2.value // => 100
let cancelable = publisher1.assign(to: \.value, on: publisher2)
// 現在値を持っているためassignした瞬間値が流れるので、publisher1,2は同じ値になる
publisher1.value // => 1
publisher2.value // => 1
publisher1.value = 2
// 上流であるpublisher1を更新するとpublisher2に値が流れる
publisher1.value // => 2
publisher2.value // => 2
publisher2.value = 200
// 下流であるpublisher2を更新してもpublisher1には影響しない
publisher1.value // => 2
publisher2.value // => 200
}
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
do {
let publisher1: UserDefaults.Publisher<Int> = .init(key: "key1", default: 1)
let publisher2: UserDefaults.Publisher<Int> = .init(key: "key2", default: 100)
let cancelable = publisher1.assign(to: \.value, on: publisher2)
publisher1.value // => 1
publisher2.value // => 1
cancelable.cancel()
publisher1.value = 2
// cancelすればpublisher1を更新してもpublisher2に値は流れない
publisher1.value // => 2
publisher2.value // => 1
}
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
struct First {
@UserDefault("number")
var number: Int = 10
}
struct Second {
@UserDefault("number")
var number: Int = 10
}
var first = First()
var second = Second()
first.$number.sink { print(#line, $0) }
first.number = 20
first.number // => 20
second.number // => 20
second.number = 30
first.number // => 30
second.number // => 30
let publisher = Publishers.Sequence<[Int], Never>(sequence: [40, 50, 60])
let cancelable = publisher.assign(to: \.value, on: first.$number)
first.number // => 60
second.number // => 60
let input: PassthroughSubject<Int, Never> = .init()
let cancellable: AnyCancellable = input.assign(to: \.value, on: first.$number)
let output: AnyPublisher<Int, Never> = publisher.eraseToAnyPublisher()
output.sink { print(#line, $0) }
input.send(70)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment