Substantially rewrite UserDefaults to:
- Add persistent domains
- Guarantee that all values returned are value types.
diff --git a/Foundation/UserDefaults.swift b/Foundation/UserDefaults.swift
index dfcd89c..73379da 100644
--- a/Foundation/UserDefaults.swift
+++ b/Foundation/UserDefaults.swift
@@ -9,139 +9,78 @@
import CoreFoundation
-private var registeredDefaults = [String: NSObject]()
+private var registeredDefaults = [String: Any]()
private var sharedDefaults = UserDefaults()
-internal func plistValueAsNSObject(_ value: Any) -> NSObject? {
- let nsValue: NSObject
-
- // Converts a value to the internal representation. Internalized values are
- // stored as NSObject derived objects in the registration dictionary.
- if let val = value as? String {
- nsValue = val._nsObject
- } else if let val = value as? URL {
- nsValue = val.path._nsObject
- } else if let val = value as? Int {
- nsValue = NSNumber(value: val)
- } else if let val = value as? Double {
- nsValue = NSNumber(value: val)
- } else if let val = value as? Bool {
- nsValue = NSNumber(value: val)
- } else if let val = value as? Data {
- nsValue = val._nsObject
- } else if let val = value as? Date {
- nsValue = val._nsObject
- } else if let val = value as? [Any] {
- var nsValues: [NSObject] = []
- for innerValue in val {
- guard let nsInnerValue = plistValueAsNSObject(innerValue) else { return nil }
- nsValues.append(nsInnerValue)
- }
- return NSArray(array: nsValues)
- } else if let val = value as? [String: Any] {
- var nsValues: [String: NSObject] = [:]
- for (key, innerValue) in val {
- guard let nsInnerValue = plistValueAsNSObject(innerValue) else { return nil }
- nsValues[key] = nsInnerValue
- }
- return NSDictionary(dictionary: nsValues)
- } else if let val = value as? NSObject {
- nsValue = val
- } else {
- return nil
- }
-
- return nsValue
-}
-
-internal func plistNSObjectAsValue(_ nsValue: NSObject) -> Any {
- let value: Any
-
- // Converts a value to the internal representation. Internalized values are
- // stored as NSObject derived objects in the registration dictionary.
- if let val = nsValue as? NSString {
- value = val._swiftObject
- } else if let val = nsValue as? NSNumber {
- value = val._swiftValueOfOptimalType
- } else if let val = nsValue as? NSData {
- value = val._swiftObject
- } else if let val = nsValue as? NSArray {
- value = val._swiftObject.map { plistNSObjectAsValue($0 as! NSObject) }
- } else if let val = nsValue as? NSDictionary {
- var values: [String: Any] = [:]
- for (currentKey, currentInnerValue) in val {
- let key: String
-
- if let swiftKey = currentKey as? String {
- key = swiftKey
- } else if let nsKey = currentKey as? NSString {
- key = nsKey._swiftObject
- } else {
- continue
- }
-
- if let nsInnerValue = currentInnerValue as? NSObject {
- values[key] = plistNSObjectAsValue(nsInnerValue)
- } else {
- values[key] = currentInnerValue
- }
- }
- value = values
- } else if let val = nsValue as? NSDate {
- value = val._swiftObject
- } else {
- value = nsValue
- }
-
- return value
-}
-
-private extension Dictionary {
- func convertingValuesToNSObjects() -> [Key: NSObject]? {
- var result: [Key: NSObject] = [:]
-
- for (key, value) in self {
- if let nsValue = plistValueAsNSObject(value) {
- result[key] = nsValue
- } else {
- return nil
- }
- }
-
- return result
- }
-}
-
-private extension Dictionary where Value == NSObject {
- mutating func merge(convertingValuesToNSObject source: [Key: Any], uniquingKeysWith block: (NSObject, NSObject) throws -> Value) rethrows -> Bool {
- if let converted = source.convertingValuesToNSObjects() {
- try self.merge(converted, uniquingKeysWith: block)
- return true
- } else {
- return false
- }
- }
-
- func convertingValuesFromPlistNSObject() -> [Key: Any] {
- var result: [Key: Any] = [:]
- for (key, value) in self {
- result[key] = plistNSObjectAsValue(value)
- }
- return result
- }
-}
-
open class UserDefaults: NSObject {
+ static private func _isValueAllowed(_ nonbridgedValue: Any) -> Bool {
+ let value = _SwiftValue.fetch(nonOptional: nonbridgedValue as AnyObject)
+
+ if let value = value as? [Any] {
+ for innerValue in value {
+ if !_isValueAllowed(innerValue) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ if let value = value as? [AnyHashable: Any] {
+ for (key, innerValue) in value {
+ if !(key is String) {
+ return false
+ }
+
+ if !_isValueAllowed(innerValue) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ let isOfCommonTypes = value is String || value is Data || value is Date || value is Int || value is Bool || value is CGFloat
+ if isOfCommonTypes {
+ return true
+ }
+
+ // NSNumber doesn't quite bridge -- treat it specially.
+ if value is NSNumber {
+ return true
+ }
+
+ let isOfUncommonNumericTypes = value is Double || value is Float || value is Float || value is Int8 || value is UInt8 || value is Int16 || value is UInt16 || value is Int32 || value is UInt32 || value is Int64 || value is UInt64
+ return isOfUncommonNumericTypes
+ }
+
+ static private func _unboxingNSNumbers(_ value: Any?) -> Any? {
+ if value == nil {
+ return nil
+ }
+
+ if let number = value as? NSNumber {
+ return number._swiftValueOfOptimalType
+ }
+
+ if let value = value as? [Any] {
+ return value.map(_unboxingNSNumbers)
+ }
+
+ if let value = value as? [AnyHashable: Any] {
+ return value.mapValues(_unboxingNSNumbers)
+ }
+
+ return value
+ }
+
private let suite: String?
open class var standard: UserDefaults {
return sharedDefaults
}
- open class func resetStandardUserDefaults() {
- //sharedDefaults.synchronize()
- //sharedDefaults = UserDefaults()
- }
+ open class func resetStandardUserDefaults() {}
public convenience override init() {
self.init(suiteName: nil)!
@@ -162,35 +101,17 @@
}
func getFromRegistered() -> Any? {
- return registeredDefaults[defaultName]
+ return UserDefaults._unboxingNSNumbers(registeredDefaults[defaultName])
}
guard let anObj = CFPreferencesCopyAppValue(defaultName._cfObject, suite?._cfObject ?? kCFPreferencesCurrentApplication) else {
return getFromRegistered()
}
- //Force the returned value to an NSObject
- switch CFGetTypeID(anObj) {
- case CFStringGetTypeID():
- return unsafeBitCast(anObj, to: NSString.self)
-
- case CFNumberGetTypeID():
- return unsafeBitCast(anObj, to: NSNumber.self)
-
- case CFURLGetTypeID():
- return unsafeBitCast(anObj, to: NSURL.self)
-
- case CFArrayGetTypeID():
- return unsafeBitCast(anObj, to: NSArray.self)
-
- case CFDictionaryGetTypeID():
- return unsafeBitCast(anObj, to: NSDictionary.self)
-
- case CFDataGetTypeID():
- return unsafeBitCast(anObj, to: NSData.self)
-
- default:
- return getFromRegistered()
+ if let fetched = _SwiftValue.fetch(anObj) {
+ return UserDefaults._unboxingNSNumbers(fetched)
+ } else {
+ return nil
}
}
@@ -200,147 +121,103 @@
return
}
- let cfType: CFTypeRef
-
- // Convert the input value to the internal representation. All values are
- // represented as CFTypeRef objects internally because we store the defaults
- // in a CFPreferences type.
- if let bType = value as? NSNumber {
- cfType = bType._cfObject
- } else if let bType = value as? NSString {
- cfType = bType._cfObject
- } else if let bType = value as? NSArray {
- cfType = bType._cfObject
- } else if let bType = value as? NSDictionary {
- cfType = bType._cfObject
- } else if let bType = value as? NSData {
- cfType = bType._cfObject
- } else if let bType = value as? NSURL {
- set(URL(reference: bType), forKey: defaultName)
+ if let url = value as? URL {
+ set(url.absoluteURL.path, forKey: defaultName)
return
- } else if let bType = value as? String {
- cfType = bType._cfObject
- } else if let bType = value as? URL {
- set(bType, forKey: defaultName)
- return
- } else if let bType = value as? Int {
- var cfValue = Int64(bType)
- cfType = CFNumberCreate(nil, kCFNumberSInt64Type, &cfValue)
- } else if let bType = value as? Double {
- var cfValue = bType
- cfType = CFNumberCreate(nil, kCFNumberDoubleType, &cfValue)
- } else if let bType = value as? Data {
- cfType = bType._cfObject
- } else {
- fatalError("The type of 'value' passed to UserDefaults.set(forKey:) is not supported.")
}
- CFPreferencesSetAppValue(defaultName._cfObject, cfType, suite?._cfObject ?? kCFPreferencesCurrentApplication)
+ if let url = value as? NSURL, let path = url.absoluteURL?.path {
+ set(path, forKey: defaultName)
+ return
+ }
+
+ guard UserDefaults._isValueAllowed(value) else {
+ fatalError("This value is not supported by set(_:forKey:)")
+ }
+
+ CFPreferencesSetAppValue(defaultName._cfObject, _SwiftValue.store(value), suite?._cfObject ?? kCFPreferencesCurrentApplication)
}
open func removeObject(forKey defaultName: String) {
CFPreferencesSetAppValue(defaultName._cfObject, nil, suite?._cfObject ?? kCFPreferencesCurrentApplication)
}
+
open func string(forKey defaultName: String) -> String? {
- guard let aVal = object(forKey: defaultName),
- let bVal = aVal as? NSString else {
- return nil
- }
- return bVal._swiftObject
+ return object(forKey: defaultName) as? String
}
+
open func array(forKey defaultName: String) -> [Any]? {
- guard let aVal = object(forKey: defaultName),
- let bVal = aVal as? NSArray else {
- return nil
- }
- return bVal._swiftObject
+ return object(forKey: defaultName) as? [Any]
}
+
open func dictionary(forKey defaultName: String) -> [String : Any]? {
- guard let aVal = object(forKey: defaultName),
- let bVal = aVal as? NSDictionary else {
- return nil
- }
- //This got out of hand fast...
- let cVal = bVal._swiftObject
- enum convErr: Swift.Error {
- case convErr
- }
- do {
- let dVal = try cVal.map({ (key, val) -> (String, Any) in
- if let strKey = key as? NSString {
- return (strKey._swiftObject, val)
- } else {
- throw convErr.convErr
- }
- })
- var eVal = [String : Any]()
-
- for (key, value) in dVal {
- eVal[key] = value
- }
-
- return eVal
- } catch _ { }
- return nil
+ return object(forKey: defaultName) as? [String: Any]
}
+
open func data(forKey defaultName: String) -> Data? {
- guard let aVal = object(forKey: defaultName),
- let bVal = aVal as? NSData else {
- return nil
- }
- return Data(referencing: bVal)
+ return object(forKey: defaultName) as? Data
}
+
open func stringArray(forKey defaultName: String) -> [String]? {
- guard let aVal = object(forKey: defaultName),
- let bVal = aVal as? NSArray else {
- return nil
- }
- return _SwiftValue.fetch(nonOptional: bVal) as? [String]
+ return object(forKey: defaultName) as? [String]
}
+
open func integer(forKey defaultName: String) -> Int {
guard let aVal = object(forKey: defaultName) else {
return 0
}
- if let bVal = aVal as? NSNumber {
- return bVal.intValue
+ if let bVal = aVal as? Int {
+ return bVal
}
- if let bVal = aVal as? NSString {
- return bVal.integerValue
+ if let bVal = aVal as? String {
+ return NSString(string: bVal).integerValue
}
return 0
}
+
open func float(forKey defaultName: String) -> Float {
guard let aVal = object(forKey: defaultName) else {
return 0
}
- if let bVal = aVal as? NSNumber {
- return bVal.floatValue
+ if let bVal = aVal as? Float {
+ return bVal
}
- if let bVal = aVal as? NSString {
- return bVal.floatValue
+ if let bVal = aVal as? String {
+ return NSString(string: bVal).floatValue
}
return 0
}
+
open func double(forKey defaultName: String) -> Double {
guard let aVal = object(forKey: defaultName) else {
return 0
}
- if let bVal = aVal as? NSNumber {
- return bVal.doubleValue
+ if let bVal = aVal as? Double {
+ return bVal
}
- if let bVal = aVal as? NSString {
- return bVal.doubleValue
+ if let bVal = aVal as? String {
+ return NSString(string: bVal).doubleValue
}
return 0
}
+
open func bool(forKey defaultName: String) -> Bool {
guard let aVal = object(forKey: defaultName) else {
return false
}
- if let bVal = aVal as? NSNumber {
- return bVal.boolValue
+ if let bVal = aVal as? Bool {
+ return bVal
}
- if let bVal = aVal as? NSString {
- return bVal.boolValue
+ if let bVal = aVal as? Int {
+ return bVal != 0
+ }
+ if let bVal = aVal as? Float {
+ return bVal != 0
+ }
+ if let bVal = aVal as? Double {
+ return bVal != 0
+ }
+ if let bVal = aVal as? String {
+ return NSString(string: bVal).boolValue
}
return false
}
@@ -349,11 +226,10 @@
return nil
}
- if let bVal = aVal as? NSURL {
- return URL(reference: bVal)
- } else if let bVal = aVal as? NSString {
- let cVal = bVal.expandingTildeInPath
-
+ if let bVal = aVal as? URL {
+ return bVal
+ } else if let bVal = aVal as? String {
+ let cVal = NSString(string: bVal).expandingTildeInPath
return URL(fileURLWithPath: cVal)
} else if let bVal = aVal as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: bVal) as? URL
@@ -392,9 +268,7 @@
}
open func register(defaults registrationDictionary: [String : Any]) {
- if !registeredDefaults.merge(convertingValuesToNSObject: registrationDictionary, uniquingKeysWith: { $1 }) {
- fatalError("The type of 'value' passed to UserDefaults.register(defaults:) is not supported.")
- }
+ registeredDefaults.merge(registrationDictionary.mapValues { _SwiftValue.fetch(nonOptional: $0 as AnyObject) }, uniquingKeysWith: { $1 })
}
open func addSuite(named suiteName: String) {
@@ -411,24 +285,28 @@
private func _dictionaryRepresentation(searchingOutsideOfSuite: Bool) -> [String: Any] {
let registeredDefaultsIfAllowed = searchingOutsideOfSuite ? registeredDefaults : [:]
- guard let aPref = CFPreferencesCopyMultiple(nil, kCFPreferencesCurrentApplication, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost),
- let bPref = (aPref._swiftObject) as? [NSString: Any] else {
- return registeredDefaultsIfAllowed
+ guard let defaultsFromDiskCF = CFPreferencesCopyMultiple(nil, suite?._cfObject ?? kCFPreferencesCurrentApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) else {
+ return registeredDefaultsIfAllowed
}
- var allDefaults = registeredDefaultsIfAllowed
- for (key, value) in bPref {
- if let value = plistValueAsNSObject(value) {
- allDefaults[key._swiftObject] = value
+ let defaultsFromDiskWithNumbersBoxed = _SwiftValue.fetch(defaultsFromDiskCF) as? [String: AnyObject] ?? [:]
+
+ if registeredDefaultsIfAllowed.count == 0 {
+ return UserDefaults._unboxingNSNumbers(defaultsFromDiskWithNumbersBoxed) as! [String: AnyObject]
+ } else {
+ var allDefaults = registeredDefaultsIfAllowed
+
+ for (key, value) in defaultsFromDiskWithNumbersBoxed {
+ allDefaults[key] = value
}
+
+ return UserDefaults._unboxingNSNumbers(allDefaults) as! [String: Any]
}
-
- return allDefaults
}
- private static let _parsedArgumentsDomain: [String: NSObject] = UserDefaults._parseArguments(ProcessInfo.processInfo.arguments).convertingValuesToNSObjects() ?? [:]
+ private static let _parsedArgumentsDomain: [String: Any] = UserDefaults._parseArguments(ProcessInfo.processInfo.arguments)
- private var _volatileDomains: [String: [String: NSObject]] = [:]
+ private var _volatileDomains: [String: [String: Any]] = [:]
private let _volatileDomainsLock = NSLock()
open var volatileDomainNames: [String] {
@@ -444,16 +322,18 @@
let domain = _volatileDomains[domainName]
_volatileDomainsLock.unlock()
- return domain?.convertingValuesFromPlistNSObject() ?? [:]
+ return domain ?? [:]
}
open func setVolatileDomain(_ domain: [String : Any], forName domainName: String) {
- _volatileDomainsLock.lock()
- var convertedDomain: [String: NSObject] = _volatileDomains[domainName] ?? [:]
- if !convertedDomain.merge(convertingValuesToNSObject: domain, uniquingKeysWith: { $1 }) {
- fatalError("The type of 'value' passed to UserDefaults.setVolatileDomain(_:forName:) is not supported.")
+ if !UserDefaults._isValueAllowed(domain) {
+ fatalError("The content of 'domain' passed to UserDefaults.setVolatileDomain(_:forName:) is not supported.")
}
- _volatileDomains[domainName] = convertedDomain
+
+ _volatileDomainsLock.lock()
+ var storedDomain: [String: Any] = _volatileDomains[domainName] ?? [:]
+ storedDomain.merge(domain, uniquingKeysWith: { $1 })
+ _volatileDomains[domainName] = storedDomain
_volatileDomainsLock.unlock()
}
@@ -469,30 +349,44 @@
open func setPersistentDomain(_ domain: [String : Any], forName domainName: String) {
if let defaults = UserDefaults(suiteName: domainName) {
+ var removedAny = false
for key in defaults._dictionaryRepresentation(searchingOutsideOfSuite: false).keys {
defaults.removeObject(forKey: key)
+ removedAny = true
}
+ var addedAny = false
for (key, value) in domain {
defaults.set(value, forKey: key)
+ addedAny = true
+ }
+
+ _ = defaults.synchronize()
+
+ if removedAny || addedAny {
+ NotificationCenter.default.post(name: UserDefaults.didChangeNotification, object: self)
}
}
-
- NotificationCenter.default.post(name: UserDefaults.didChangeNotification, object: self)
}
open func removePersistentDomain(forName domainName: String) {
if let defaults = UserDefaults(suiteName: domainName) {
+ var removedAny = false
for key in defaults._dictionaryRepresentation(searchingOutsideOfSuite: false).keys {
defaults.removeObject(forKey: key)
+ removedAny = true
}
- NotificationCenter.default.post(name: UserDefaults.didChangeNotification, object: self)
+ _ = defaults.synchronize()
+
+ if removedAny {
+ NotificationCenter.default.post(name: UserDefaults.didChangeNotification, object: self)
+ }
}
}
open func synchronize() -> Bool {
- return CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication)
+ return CFPreferencesAppSynchronize(suite?._cfObject ?? kCFPreferencesCurrentApplication)
}
open func objectIsForced(forKey key: String) -> Bool {
diff --git a/TestFoundation/TestUserDefaults.swift b/TestFoundation/TestUserDefaults.swift
index 4ad257f..dc3f657 100644
--- a/TestFoundation/TestUserDefaults.swift
+++ b/TestFoundation/TestUserDefaults.swift
@@ -293,7 +293,7 @@
func test_persistentDomain() {
let int = (key: "An Integer", value: 1234)
- let double = (key: "A Double", value: 5678.0)
+ let double = (key: "A Double", value: 5678.1234)
let string = (key: "A String", value: "Some string")
let array = (key: "An Array", value: [ 1, 2, 3, 4, "Surprise" ] as [AnyHashable])
let dictionary = (key: "A Dictionary", value: [ "Swift": "Imperative", "Haskell": "Functional", "LISP": "LISP", "Today": Date() ] as [String: AnyHashable])
@@ -303,30 +303,37 @@
let defaults1 = UserDefaults(suiteName: nil)!
defaults1.removePersistentDomain(forName: domainName)
- XCTAssertNil(defaults1.persistentDomain(forName: domainName))
+ if let domain = defaults1.persistentDomain(forName: domainName) {
+ XCTAssertEqual(domain.count, 0)
+ } // else it's nil, which is also OK.
- defaults1.setPersistentDomain([int.key: int.value,
- double.key: double.value,
- string.key: string.value,
- array.key: array.value,
- dictionary.key: dictionary.value],
- forName: domainName)
+ let defaultsIn: [String : Any] =
+ [ int.key: int.value,
+ double.key: double.value,
+ string.key: string.value,
+ array.key: array.value,
+ dictionary.key: dictionary.value ]
+
+ defaults1.setPersistentDomain(defaultsIn, forName: domainName)
let defaults2 = UserDefaults(suiteName: nil)!
let returned = defaults2.persistentDomain(forName: domainName)
XCTAssertNotNil(returned)
if let returned = returned {
- XCTAssertEqual(returned[int.key] as! Int, int.value)
- XCTAssertEqual(returned[double.key] as! Double, double.value)
- XCTAssertEqual(returned[string.key] as! String, string.value)
- XCTAssertEqual(returned[array.key] as! [AnyHashable], array.value)
- XCTAssertEqual(returned[dictionary.key] as! [String: AnyHashable], dictionary.value)
+ XCTAssertEqual(returned.count, defaultsIn.count)
+ XCTAssertEqual(returned[int.key] as? Int, int.value)
+ XCTAssertEqual(returned[double.key] as? Double, double.value)
+ XCTAssertEqual(returned[string.key] as? String, string.value)
+ XCTAssertEqual(returned[array.key] as? [AnyHashable], array.value)
+ XCTAssertEqual(returned[dictionary.key] as? [String: AnyHashable], dictionary.value)
}
- defaults2.removeSuite(named: domainName)
+ defaults2.removePersistentDomain(forName: domainName)
let defaults3 = UserDefaults(suiteName: nil)!
- XCTAssertNil(defaults3.persistentDomain(forName: domainName))
+ if let domain = defaults3.persistentDomain(forName: domainName) {
+ XCTAssertEqual(domain.count, 0)
+ } // else it's nil, which is also OK.
}
}