Merge pull request #1408 from millenomi/ud-persistent-domains
diff --git a/Foundation/NSNumber.swift b/Foundation/NSNumber.swift
index 330f081..f7f257e 100644
--- a/Foundation/NSNumber.swift
+++ b/Foundation/NSNumber.swift
@@ -622,6 +622,34 @@
fatalError("unsupported CFNumberType: '\(numberType)'")
}
}
+
+ internal var _swiftValueOfOptimalType: Any {
+ if self === kCFBooleanTrue {
+ return true
+ } else if self === kCFBooleanFalse {
+ return false
+ }
+
+ let numberType = _CFNumberGetType2(_cfObject)
+ switch numberType {
+ case kCFNumberSInt8Type:
+ return Int(int8Value)
+ case kCFNumberSInt16Type:
+ return Int(int16Value)
+ case kCFNumberSInt32Type:
+ return Int(int32Value)
+ case kCFNumberSInt64Type:
+ return int64Value < Int.max ? Int(int64Value) : int64Value
+ case kCFNumberFloat32Type:
+ return floatValue
+ case kCFNumberFloat64Type:
+ return doubleValue
+ case kCFNumberSInt128Type:
+ return int128Value
+ default:
+ fatalError("unsupported CFNumberType: '\(numberType)'")
+ }
+ }
deinit {
_CFDeinit(self)
diff --git a/Foundation/UserDefaults.swift b/Foundation/UserDefaults.swift
index 9539829..509da53 100644
--- a/Foundation/UserDefaults.swift
+++ b/Foundation/UserDefaults.swift
@@ -12,17 +12,85 @@
private var registeredDefaults = [String: Any]()
private var sharedDefaults = UserDefaults()
+fileprivate func bridgeFromNSCFTypeIfNeeded(_ value: Any) -> Any {
+ // This line will produce a 'Conditional cast always succeeds' warning if compoiled on Darwin, since Darwin has bridging casts of any value to an object,
+ // but is required for non-Darwin to work correctly, since that platform _doesn't_ have bridging casts of that kind for now.
+ if let object = value as? AnyObject {
+ return _SwiftValue.fetch(nonOptional: object)
+ } else {
+ return value
+ }
+}
+
open class UserDefaults: NSObject {
+ static private func _isValueAllowed(_ nonbridgedValue: Any) -> Bool {
+ let value = bridgeFromNSCFTypeIfNeeded(nonbridgedValue)
+
+ 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
+ }
+
+ // NSNumber doesn't quite bridge -- treat it specially.
+ if value is NSNumber {
+ 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
+ }
+
+ 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)!
@@ -31,39 +99,29 @@
/// nil suite means use the default search list that +standardUserDefaults uses
public init?(suiteName suitename: String?) {
suite = suitename
+ super.init()
+
+ setVolatileDomain(UserDefaults._parsedArgumentsDomain, forName: UserDefaults.argumentDomain)
}
open func object(forKey defaultName: String) -> Any? {
+ let argumentDomain = volatileDomain(forName: UserDefaults.argumentDomain)
+ if let object = argumentDomain[defaultName] {
+ return object
+ }
+
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
}
}
@@ -73,147 +131,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
}
@@ -222,11 +236,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
@@ -265,31 +278,7 @@
}
open func register(defaults registrationDictionary: [String : Any]) {
- for (key, value) in registrationDictionary {
- 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? NSObject {
- nsValue = val
- } else {
- fatalError("The type of 'value' passed to UserDefaults.register(defaults:) is not supported.")
- }
-
- registeredDefaults[key] = nsValue
- }
+ registeredDefaults.merge(registrationDictionary.mapValues(bridgeFromNSCFTypeIfNeeded), uniquingKeysWith: { $1 })
}
open func addSuite(named suiteName: String) {
@@ -299,31 +288,105 @@
CFPreferencesRemoveSuitePreferencesFromApp(kCFPreferencesCurrentApplication, suiteName._cfObject)
}
- open func dictionaryRepresentation() -> [String : Any] {
- guard let aPref = CFPreferencesCopyMultiple(nil, kCFPreferencesCurrentApplication, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost),
- let bPref = (aPref._swiftObject) as? [NSString: Any] else {
- return registeredDefaults
- }
- var allDefaults = registeredDefaults
-
- for (key, value) in bPref {
- allDefaults[key._swiftObject] = value
- }
-
- return allDefaults
+ open func dictionaryRepresentation() -> [String: Any] {
+ return _dictionaryRepresentation(includingVolatileDomains: true)
}
- open var volatileDomainNames: [String] { NSUnimplemented() }
- open func volatileDomain(forName domainName: String) -> [String : Any] { NSUnimplemented() }
- open func setVolatileDomain(_ domain: [String : Any], forName domainName: String) { NSUnimplemented() }
- open func removeVolatileDomain(forName domainName: String) { NSUnimplemented() }
+ private func _dictionaryRepresentation(includingVolatileDomains: Bool) -> [String: Any] {
+ let registeredDefaultsIfAllowed = includingVolatileDomains ? registeredDefaults : [:]
+
+ guard let defaultsFromDiskCF = CFPreferencesCopyMultiple(nil, suite?._cfObject ?? kCFPreferencesCurrentApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) else {
+ return registeredDefaultsIfAllowed
+ }
+
+ let defaultsFromDiskWithNumbersBoxed = _SwiftValue.fetch(defaultsFromDiskCF) as? [String: Any] ?? [:]
+
+ if registeredDefaultsIfAllowed.count == 0 {
+ return UserDefaults._unboxingNSNumbers(defaultsFromDiskWithNumbersBoxed) as! [String: Any]
+ } else {
+ var allDefaults = registeredDefaultsIfAllowed
+
+ for (key, value) in defaultsFromDiskWithNumbersBoxed {
+ allDefaults[key] = value
+ }
+
+ return UserDefaults._unboxingNSNumbers(allDefaults) as! [String: Any]
+ }
+ }
- open func persistentDomain(forName domainName: String) -> [String : Any]? { NSUnimplemented() }
- open func setPersistentDomain(_ domain: [String : Any], forName domainName: String) { NSUnimplemented() }
- open func removePersistentDomain(forName domainName: String) { NSUnimplemented() }
+ private static let _parsedArgumentsDomain: [String: Any] = UserDefaults._parseArguments(ProcessInfo.processInfo.arguments)
+
+ private var _volatileDomains: [String: [String: Any]] = [:]
+ private let _volatileDomainsLock = NSLock()
+
+ open var volatileDomainNames: [String] {
+ _volatileDomainsLock.lock()
+ let names = Array(_volatileDomains.keys)
+ _volatileDomainsLock.unlock()
+
+ return names
+ }
+
+ open func volatileDomain(forName domainName: String) -> [String : Any] {
+ _volatileDomainsLock.lock()
+ let domain = _volatileDomains[domainName]
+ _volatileDomainsLock.unlock()
+
+ return domain ?? [:]
+ }
+
+ open func setVolatileDomain(_ domain: [String : Any], forName domainName: String) {
+ if !UserDefaults._isValueAllowed(domain) {
+ fatalError("The content of 'domain' passed to UserDefaults.setVolatileDomain(_:forName:) is not supported.")
+ }
+
+ _volatileDomainsLock.lock()
+ var storedDomain: [String: Any] = _volatileDomains[domainName] ?? [:]
+ storedDomain.merge(domain, uniquingKeysWith: { $1 })
+ _volatileDomains[domainName] = storedDomain
+ _volatileDomainsLock.unlock()
+ }
+
+ open func removeVolatileDomain(forName domainName: String) {
+ _volatileDomainsLock.lock()
+ _volatileDomains.removeValue(forKey: domainName)
+ _volatileDomainsLock.unlock()
+ }
+
+ open func persistentDomain(forName domainName: String) -> [String : Any]? {
+ return UserDefaults(suiteName: domainName)?._dictionaryRepresentation(includingVolatileDomains: false)
+ }
+
+ open func setPersistentDomain(_ domain: [String : Any], forName domainName: String) {
+ if let defaults = UserDefaults(suiteName: domainName) {
+ for key in defaults._dictionaryRepresentation(includingVolatileDomains: false).keys {
+ defaults.removeObject(forKey: key)
+ }
+
+ for (key, value) in domain {
+ defaults.set(value, forKey: key)
+ }
+
+ _ = defaults.synchronize()
+
+ NotificationCenter.default.post(name: UserDefaults.didChangeNotification, object: self)
+ }
+ }
+
+ open func removePersistentDomain(forName domainName: String) {
+ if let defaults = UserDefaults(suiteName: domainName) {
+ for key in defaults._dictionaryRepresentation(includingVolatileDomains: false).keys {
+ defaults.removeObject(forKey: key)
+ }
+
+ _ = defaults.synchronize()
+
+ 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 {
@@ -345,3 +408,54 @@
public static let argumentDomain: String = "NSArgumentDomain"
public static let registrationDomain: String = "NSRegistrationDomain"
}
+
+// MARK: -
+// MARK: Parsing arguments.
+
+fileprivate let propertyListPrefixes: Set<Character> = [ "{", "[", "(", "<", "\"" ]
+
+private extension UserDefaults {
+ static func _parseArguments(_ arguments: [String]) -> [String: Any] {
+ var result: [String: Any] = [:]
+
+ let count = arguments.count
+
+ var index = 0
+ while index < count - 1 { // We're looking for pairs, so stop at the second-to-last argument.
+ let current = arguments[index]
+ let next = arguments[index + 1]
+ if current.hasPrefix("-") && !next.hasPrefix("-") {
+ // Match what Darwin does, which is to check whether the first argument is one of the characters that make up a NeXTStep-style or XML property list: open brace, open parens, open bracket, open angle bracket, or double quote. If it is, attempt parsing it as a plist; otherwise, just use the argument value as a String.
+
+ let keySubstring = current[current.index(after: current.startIndex)...]
+ if !keySubstring.isEmpty {
+ let key = String(keySubstring)
+ let value = next.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+
+ var parsed = false
+ if let prefix = value.first, propertyListPrefixes.contains(prefix) {
+ if let data = value.data(using: .utf8),
+ let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) {
+
+ // If we can parse that argument as a plist, use the parsed value.
+ parsed = true
+ result[key] = plist
+
+ }
+ }
+
+ if !parsed {
+ result[key] = value
+ }
+ }
+
+ index += 1 // Skip both the key and the value on this loop.
+ }
+
+ index += 1
+ }
+
+ return result
+ }
+}
+
diff --git a/TestFoundation/TestUserDefaults.swift b/TestFoundation/TestUserDefaults.swift
index d05e68b..132871e 100644
--- a/TestFoundation/TestUserDefaults.swift
+++ b/TestFoundation/TestUserDefaults.swift
@@ -38,6 +38,8 @@
("test_setValue_BoolFromString", test_setValue_BoolFromString ),
("test_setValue_IntFromString", test_setValue_IntFromString ),
("test_setValue_DoubleFromString", test_setValue_DoubleFromString ),
+ ("test_volatileDomains", test_volatileDomains),
+ ("test_persistentDomain", test_persistentDomain ),
]
}
@@ -254,4 +256,103 @@
XCTAssertEqual(defaults.double(forKey: "key1"), 12.34)
}
+
+ func test_volatileDomains() {
+ let dateKey = "A Date",
+ stringKey = "A String",
+ arrayKey = "An Array",
+ dictionaryKey = "A Dictionary",
+ dataKey = "Some Data",
+ boolKey = "A Bool"
+
+ let defaultsIn: [String: Any] = [
+ dateKey: Date(),
+ stringKey: "The String",
+ arrayKey: [1, 2, 3],
+ dictionaryKey: ["Swift": "Imperative", "Haskell": "Functional", "LISP": "LISP", "Today": Date()],
+ dataKey: "The Data".data(using: .utf8)!,
+ boolKey: true
+ ]
+
+ let domainName = "TestDomain"
+
+ let defaults = UserDefaults(suiteName: nil)!
+ XCTAssertFalse(defaults.volatileDomainNames.contains(domainName))
+
+ defaults.setVolatileDomain(defaultsIn, forName: domainName)
+ let defaultsOut = defaults.volatileDomain(forName: domainName)
+
+ XCTAssertEqual(defaultsIn.count, defaultsOut.count)
+ XCTAssertEqual(defaultsIn[dateKey] as! Date, defaultsOut[dateKey] as! Date)
+ XCTAssertEqual(defaultsIn[stringKey] as! String, defaultsOut[stringKey] as! String)
+ XCTAssertEqual(defaultsIn[arrayKey] as! [Int], defaultsOut[arrayKey] as! [Int])
+ XCTAssertEqual(defaultsIn[dictionaryKey] as! [String: AnyHashable], defaultsOut[dictionaryKey] as! [String: AnyHashable])
+ XCTAssertEqual(defaultsIn[dataKey] as! Data, defaultsOut[dataKey] as! Data)
+ XCTAssertEqual(defaultsIn[boolKey] as! Bool, defaultsOut[boolKey] as! Bool)
+ }
+
+ func test_persistentDomain() {
+ let int = (key: "An Integer", value: 1234)
+ 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])
+
+ let domainName = "org.swift.Foundation.TestPersistentDomainName"
+
+ let done = expectation(description: "All notifications have fired.")
+
+ var countOfFiredNotifications = 0
+ let expectedNotificationCount = 3
+
+ let observer = NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { (_) in
+ countOfFiredNotifications += 1
+
+ if countOfFiredNotifications == expectedNotificationCount {
+ done.fulfill()
+ } else if countOfFiredNotifications > expectedNotificationCount {
+ XCTFail("Too many UserDefaults.didChangeNotification notifications posted.")
+ }
+ }
+
+ let defaults1 = UserDefaults(suiteName: nil)!
+
+ defaults1.removePersistentDomain(forName: domainName)
+ if let domain = defaults1.persistentDomain(forName: domainName) {
+ XCTAssertEqual(domain.count, 0)
+ } // else it's nil, which is also OK.
+
+ 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.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.removePersistentDomain(forName: domainName)
+
+ let defaults3 = UserDefaults(suiteName: nil)!
+ if let domain = defaults3.persistentDomain(forName: domainName) {
+ XCTAssertEqual(domain.count, 0)
+ } // else it's nil, which is also OK.
+
+ waitForExpectations(timeout: 10)
+
+ NotificationCenter.default.removeObserver(observer)
+ }
}