// MIT License © Sindre Sorhus import Foundation public final class Defaults { public class Keys { public typealias Key = Defaults.Key public typealias OptionalKey = Defaults.OptionalKey fileprivate init() {} } public final class Key: Keys { public let name: String public let defaultValue: T public let suite: UserDefaults public init(_ key: String, default defaultValue: T, suite: UserDefaults = .standard) { self.name = key self.defaultValue = defaultValue self.suite = suite super.init() // Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding. if UserDefaults.isNativelySupportedType(T.self) { suite.register(defaults: [key: defaultValue]) } else if let value = suite._encode(defaultValue) { suite.register(defaults: [key: value]) } } } public final class OptionalKey: Keys { public let name: String public let suite: UserDefaults public init(_ key: String, suite: UserDefaults = .standard) { self.name = key self.suite = suite } } fileprivate init() {} public subscript(key: Defaults.Key) -> T { get { return key.suite[key] } set { key.suite[key] = newValue } } public subscript(key: Defaults.OptionalKey) -> T? { get { return key.suite[key] } set { key.suite[key] = newValue } } public func clear(suite: UserDefaults = .standard) { for key in suite.dictionaryRepresentation().keys { suite.removeObject(forKey: key) } } } // Has to be `defaults` lowercase until Swift supports static subscripts… public let defaults = Defaults() extension UserDefaults { private func _get(_ key: String) -> T? { if UserDefaults.isNativelySupportedType(T.self) { return object(forKey: key) as? T } guard let text = string(forKey: key), let data = "[\(text)]".data(using: .utf8) else { return nil } do { return (try JSONDecoder().decode([T].self, from: data)).first } catch { print(error) } return nil } fileprivate func _encode(_ value: T) -> String? { do { // Some codable values like URL and enum are encoded as a top-level // string which JSON can't handle, so we need to wrap it in an array // We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750 let data = try JSONEncoder().encode([value]) return String(String(data: data, encoding: .utf8)!.dropFirst().dropLast()) } catch { print(error) return nil } } private func _set(_ key: String, to value: T) { if UserDefaults.isNativelySupportedType(T.self) { set(value, forKey: key) return } set(_encode(value), forKey: key) } public subscript(key: Defaults.Key) -> T { get { return _get(key.name) ?? key.defaultValue } set { _set(key.name, to: newValue) } } public subscript(key: Defaults.OptionalKey) -> T? { get { return _get(key.name) } set { guard let value = newValue else { set(nil, forKey: key.name) return } _set(key.name, to: value) } } fileprivate static func isNativelySupportedType(_ type: T.Type) -> Bool { switch type { case is Bool.Type, is String.Type, is Int.Type, is Double.Type, is Float.Type, is Date.Type, is Data.Type: return true default: return false } } }