Swift 3: NSArray and NSDictionary, type inference, and writing to plists

There seems to be some inconsistencies with how the types of elements of NSArrays, keys of NSDictionaries, and values of NSDictionaries are inferred, and whether the .write(toFile:atomically:) works.


NSArrays where all the elements are literal strings, all the elements are Strings, or all the elements are NSStrings are all written correctly by the .write function. This implies that Swift Strings are property list objects because the docs say "This method recursively validates that all the contained objects are property list objects before writing out the file, and returns false if all the objects are not property list objects, since the resultant file would not be a valid property list."


NSDictionaries with all NSString keys, and values of types Optional Dates, NSNumbers, and Optional Strings are also successfully written by the .write function IF the values are added one at a time to an NSMutableDictionary. This implies that those Optional Dates and Optional Strings are property list objects because the docs say "This method recursively validates that all the contained objects are property list objects (instances of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary) before writing out the file, and returns false if all the objects are not property list objects...".


NSArrays filled with a mixture of Optional Dates, NSNumbers, and Optional Strings are not written successfully by the .write function. Why not, if an NSDictionary with those values can be written?


NSDictionaries with the same value types as mentioned above, built by adding values one at a time to an NSMutableDictionary, but with keys that are string literals or Swift Strings are not written successfully by the .write function. Why not, if NSArrays with string literals or Swift Strings can be written?


NSDictionaries with the same value types as mentioned above, but intialized with [<key:value pairs>] instead of by adding them one at a time to an NSMutableDictionary, are not written successfully, even if the keys are NSStrings. Note that the type of the dictionary values printed is different when the keys are NSStrings and the values are added one by one, compared to when the keys are any of literal, String, or NSString and the values are initialized with [<key:value pairs>].


There are also some other differences in the optionality of the values, depending on how they are added to the dictionary.


Can anyone shed some light on this? It seems to be inconsistent at best and a bug at worst.


This requires a lot of code and output to demonstrate, so here goes:

import UIKit

let first = "First"
let second = "Second"
let third = "Third"
let nsFirst: NSString = "First"
let nsSecond: NSString = "Second"
let nsThird: NSString = "Third"

class ViewController: UIViewController {
    private var appSupportDirectory: String!
   
    func printIt(collectionName: String, collection: Any, saved:Bool) {
        print(collectionName + ": \(collection)")
        print("Collection type: \(type(of: collection))")
        if collection is NSArray {
            let aArray = collection as! NSArray
            print("First element type: \(type(of: aArray[0]))")
        } else {
            let aDict = collection as! NSMutableDictionary
            for k in aDict.allKeys {
                print("First key type: \(type(of: k))")
                print("First value type: \(type(of: aDict[k]!))")
                break
            }
        }
        print("Saved: \(saved)")
        print()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        /
        let directoriesArray = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
        self.appSupportDirectory = directoriesArray[0]
        let fMgr = FileManager.default
        if !fMgr.fileExists(atPath: self.appSupportDirectory) {
            do {
                try fMgr.createDirectory(atPath: self.appSupportDirectory, withIntermediateDirectories: true, attributes: nil)
            } catch {
                preconditionFailure("Could not create Application Support directory!")
            }
        }
       
        let literalArray: NSMutableArray = ["First", "Second", "Third"]
        let literalArrayFileName = self.appSupportDirectory + "/literalArray.plist"
        var saved = literalArray.write(toFile: literalArrayFileName, atomically: true)
        printIt(collectionName: "Literal Array", collection: literalArray, saved: saved)
       
        let globalStringsArray: NSMutableArray = [first, second, third]
        let globalStringsArrayFileName = self.appSupportDirectory + "/globalStringsArray.plist"
        saved = globalStringsArray.write(toFile: globalStringsArrayFileName, atomically: true)
        printIt(collectionName: "Global Strings Array", collection: globalStringsArray, saved: saved)
       
        let globalNSStringsArray: NSMutableArray = [nsFirst, nsSecond, nsThird]
        let globalNSStringsArrayFileName = self.appSupportDirectory + "/globalNSStringsArray.plist"
        saved = globalNSStringsArray.write(toFile: globalNSStringsArrayFileName, atomically: true)
        printIt(collectionName: "Global NSStrings Array", collection: globalNSStringsArray, saved: saved)
       
        let aDate: Date? = Date()
        let aNumber = NSNumber(value: true)
        let aString: String? = "3"

        let initializedDirectArray: NSMutableArray = [Date() as Date?, NSNumber(value: true), "3" as String?]
        let initializedDirectArrayFileName = self.appSupportDirectory + "/initializedDirectArray.plist"
        saved = initializedDirectArray.write(toFile: initializedDirectArrayFileName, atomically: true)
        printIt(collectionName: "Array initialized with direct values", collection: initializedDirectArray, saved: saved)
       
        let initializedVariableArray: NSMutableArray = [aDate, aNumber, aString]
        let initializedVariableArrayFileName = self.appSupportDirectory + "/initializedVariableArray.plist"
        saved = initializedVariableArray.write(toFile: initializedVariableArrayFileName, atomically: true)
        printIt(collectionName: "Array initialized with variable values", collection: initializedVariableArray, saved: saved)
       
        let builtDirectArray = NSMutableArray()
        builtDirectArray.add(Date() as Date?)
        builtDirectArray.add(NSNumber(value: true))
        builtDirectArray.add("3" as String?)
        let builtDirectArrayFileName = self.appSupportDirectory + "/builtDirectArray.plist"
        saved = builtDirectArray.write(toFile: builtDirectArrayFileName, atomically: true)
        printIt(collectionName: "Array built with direct values", collection: builtDirectArray, saved: saved)
       
        let builtVariableArray = NSMutableArray()
        builtVariableArray.add(Date() as Date?)
        builtVariableArray.add(NSNumber(value: true))
        builtVariableArray.add("3" as String?)
        let builtVariableArrayFileName = self.appSupportDirectory + "/builtVariableArray.plist"
        saved = builtVariableArray.write(toFile: builtVariableArrayFileName, atomically: true)
        printIt(collectionName: "Array built with variable values", collection: builtVariableArray, saved: saved)

        let literalDictionaryFileName = self.appSupportDirectory + "/literalDictionary.plist"
        var literalDictionary: NSMutableDictionary = ["First" : Date() as NSDate?, "Second" : NSNumber(value: true), "Third" : "3" as String?]
        saved = literalDictionary.write(toFile: literalDictionaryFileName, atomically: true)
        printIt(collectionName: "Literal Keys Dictionary initialized with direct values", collection: literalDictionary, saved: saved)
        literalDictionary = ["First" : aDate, "Second" : aNumber, "Third" : aString]
        saved = literalDictionary.write(toFile: literalDictionaryFileName, atomically: true)
        printIt(collectionName: "Literal Keys Dictionary initialized with variable values", collection: literalDictionary, saved: saved)

        literalDictionary = NSMutableDictionary()
        literalDictionary["First"] = Date() as NSDate?
        literalDictionary["Second"] = NSNumber(value: true)
        literalDictionary["Third"] = "3" as String?
        saved = literalDictionary.write(toFile: literalDictionaryFileName, atomically: true)
        printIt(collectionName: "Literal Keys Dictionary built with direct values", collection: literalDictionary, saved: saved)
        literalDictionary = NSMutableDictionary()
        literalDictionary["First"] = aDate
        literalDictionary["Second"] = aNumber
        literalDictionary["Third"] = aString
        saved = literalDictionary.write(toFile: literalDictionaryFileName, atomically: true)
        printIt(collectionName: "Literal Keys Dictionary built with variable values", collection: literalDictionary, saved: saved)
       
        let globalStringsDictionaryFileName = self.appSupportDirectory + "/globalStringsDictionary.plist"
        var globalStringsDictionary: NSMutableDictionary = [first : Date() as NSDate?, second : NSNumber(value: true), third : "3" as String?]
        saved = globalStringsDictionary.write(toFile: globalStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global String Keys Dictionary initialized with direct values", collection: globalStringsDictionary, saved: saved)

        globalStringsDictionary = [first : aDate, second : aNumber, third : aString]
        saved = globalStringsDictionary.write(toFile: globalStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global String Keys Dictionary initialized with variable values", collection: globalStringsDictionary, saved: saved)

        globalStringsDictionary = NSMutableDictionary()
        globalStringsDictionary[first] = Date() as NSDate?
        globalStringsDictionary[second] = NSNumber(value: true)
        globalStringsDictionary[third] = "3" as String?
        saved = globalStringsDictionary.write(toFile: globalStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global String Keys Dictionary built with direct values", collection: globalStringsDictionary, saved: saved)

        globalStringsDictionary = NSMutableDictionary()
        globalStringsDictionary[first] = aDate
        globalStringsDictionary[second] = aNumber
        globalStringsDictionary[third] = aString
        saved = globalStringsDictionary.write(toFile: globalStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global String Keys Dictionary built with variable values", collection: globalStringsDictionary, saved: saved)
       
        let globalNSStringsDictionaryFileName = self.appSupportDirectory + "/globalStringsNSDictionary.plist"
        var globalNSStringsDictionary: NSMutableDictionary = [nsFirst : Date() as NSDate?, nsSecond : NSNumber(value: true), nsThird : "3" as String?]
        saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global NSString Keys Dictionary initialized with direct values", collection: globalNSStringsDictionary, saved: saved)

        globalNSStringsDictionary = [nsFirst : aDate, nsSecond : aNumber, nsThird : aString]
        saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global NSString Keys Dictionary initialized with variable values", collection: globalNSStringsDictionary, saved: saved)

        globalNSStringsDictionary = NSMutableDictionary()
        globalNSStringsDictionary[nsFirst] = Date() as NSDate?
        globalNSStringsDictionary[nsSecond] = NSNumber(value: true)
        globalNSStringsDictionary[nsThird] = "3" as String?
        saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global NSString Keys Dictionary built with direct values", collection: globalNSStringsDictionary, saved: saved)
       
        globalNSStringsDictionary = NSMutableDictionary()
        globalNSStringsDictionary[nsFirst] = aDate
        globalNSStringsDictionary[nsSecond] = aNumber
        globalNSStringsDictionary[nsThird] = aString
        saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
        printIt(collectionName: "Global NSString Keys Dictionary built with variable values", collection: globalNSStringsDictionary, saved: saved)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}


The output is:

Literal Array: (
    First,
    Second,
    Third
)
Collection type: __NSArrayM
First element type: _NSContiguousString
Saved: true


Global Strings Array: (
    First,
    Second,
    Third
)
Collection type: __NSArrayM
First element type: _NSContiguousString
Saved: true


Global NSStrings Array: (
    First,
    Second,
    Third
)
Collection type: __NSArrayM
First element type: __NSCFString
Saved: true


Array initialized with direct values: (
    "Optional(2016-08-26 16:49:18 +0000)",
    1,
    "Optional(\"3\")"
)
Collection type: __NSArrayM
First element type: _SwiftValue
Saved: false


Array initialized with variable values: (
    "Optional(2016-08-26 16:49:18 +0000)",
    1,
    "Optional(\"3\")"
)
Collection type: __NSArrayM
First element type: _SwiftValue
Saved: false


Array built with direct values: (
    "Optional(2016-08-26 16:49:18 +0000)",
    1,
    "Optional(\"3\")"
)
Collection type: __NSArrayM
First element type: _SwiftValue
Saved: false


Array built with variable values: (
    "Optional(2016-08-26 16:49:18 +0000)",
    1,
    "Optional(\"3\")"
)
Collection type: __NSArrayM
First element type: _SwiftValue
Saved: false


Literal Keys Dictionary initialized with direct values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Literal Keys Dictionary initialized with variable values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Literal Keys Dictionary built with direct values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = "Optional(1)";
    Third = "Optional(3)";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Literal Keys Dictionary built with variable values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = "Optional(1)";
    Third = "Optional(3)";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global String Keys Dictionary initialized with direct values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global String Keys Dictionary initialized with variable values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global String Keys Dictionary built with direct values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = "Optional(1)";
    Third = "Optional(3)";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global String Keys Dictionary built with variable values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = "Optional(1)";
    Third = "Optional(3)";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global NSString Keys Dictionary initialized with direct values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global NSString Keys Dictionary initialized with variable values: {
    First = "Optional(2016-08-26 16:49:18 +0000)";
    Second = 1;
    Third = "Optional(\"3\")";
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: _SwiftValue
Saved: false


Global NSString Keys Dictionary built with direct values: {
    First = "2016-08-26 16:49:18 +0000";
    Second = 1;
    Third = 3;
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: __NSDate
Saved: true


Global NSString Keys Dictionary built with variable values: {
    First = "2016-08-26 16:49:18 +0000";
    Second = 1;
    Third = 3;
}
Collection type: __NSDictionaryM
First key type: __NSCFString
First value type: __NSDate
Saved: true

You have a couple of incorrect assumptions in all this.


First, this has very little to do with type inference. Instead it has to do with bridging between Swift and Obj-C types and (new in late Swift 3 betas) to do with reference "wrapping" in the new value types. (I mean, wrapping of an object reference in a value type to give it value semantics. This was always done with Array, Dictionary, Set and String objects that originated in Obj-C code, and is now done for a lot of other new Foundation types such as Data, Date, etc.)


Second, if a collection validates as plist-compatible, that doesn't mean that any native Swift types can be assumed to be plist-compatible. It likely means that the native Swift values were bridged to Obj-C references, either when the collection was created or during the plist validation.


Third, assigning an optional value in a bridging context may not result in an optional value being added to the collection. If the optional value is not nil, the actual value may be added instead. (I don't know if there's any API contract about individual optionals and bridging, but it seems murky at best.) If you force-construct a collection with a mixture of optional and non-optional values, the collection cannot be bridged to pure Obj-C, because there's no way to represent the optionality in Obj-C.


Finally, it looks like your writes failed in all and only the cases where the value type in the collection was _SwiftValue. That makes sense to me — the only way non-Obj-C values can be represented in an Obj-C collection would be to wrap them in an opaque class type that "boxes" the native Swift value. That would prevent the collection from being plist-compatible.


OTOH, you should probably submit a bug report about this. If nothing else, it ought to be possible in principle to extend the plist serialization APIs to allow the native Swift equivalents of Obj-C serializable classes, and/or to add the serialization APIs to the Swift native Foundation module. (Perhaps the latter is already true, but you just defeated it by forcing the use of NSMutableXXX types.)

I thought about how bridging related to this, but I didn't end up mentioning it in my post. I know that bridging between Swift types and Obj-C types was supposedly removed, so I thought maybe it had more to do with type inferrence.


"It likely means that the native Swift values were bridged to Obj-C references, either when the collection was created or during the plist validation."

"I don't know if there's any API contract about individual optionals and bridging, but it seems murky at best."


I guess that's the general idea I was getting at - things are inconsistent/murky and don't seem to be well documented.


I wasn't surprised that the values that had a type of _SwiftValue couldn't be written, I was trying to demonstrate that whether they were treated as _SwiftValue or various other things is not consistent.


I probably will file a bug, at least for what seems to me to be the worst aspect, and the one that actually got me started investigating this: the first two writes in the following example fail, but the third succeeds.


let first = "First"
let second = "Second"
let third = "Third"

let nsFirst: NSString = "First"
let nsSecond: NSString = "Second"
let nsThird: NSString = "Third"

let aDate: Date? = Date()
let aNumber = NSNumber(value: true)
let aString: String? = "3"

let globalNSStringsDictionaryFileName = self.appSupportDirectory + "/globalNSStringsDictionary.plist"
let globalNSStringsDictionaryFileName = self.appSupportDirectory + "/globalStringsDictionary.plist"

var globalStringsDictionary = NSMutableDictionary()
globalStringsDictionary[first] = aDate
globalStringsDictionary[second] = aNumber
globalStringsDictionary[third] = aString
saved = globalStringsDictionary.write(toFile: globalStringsDictionaryFileName, atomically: true)
printIt(collectionName: "Global String Keys Dictionary built with variable values", collection: globalStringsDictionary, saved: saved)

var globalNSStringsDictionary: NSMutableDictionary = [nsFirst : aDate, nsSecond : aNumber, nsThird : aString]
saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
printIt(collectionName: "Global NSString Keys Dictionary initialized with variable values", collection: globalNSStringsDictionary, saved: saved)

globalNSStringsDictionary = NSMutableDictionary()
globalNSStringsDictionary[nsFirst] = aDate
globalNSStringsDictionary[nsSecond] = aNumber
globalNSStringsDictionary[nsThird] = aString
saved = globalNSStringsDictionary.write(toFile: globalNSStringsDictionaryFileName, atomically: true)
printIt(collectionName: "Global NSString Keys Dictionary built with variable values", collection: globalNSStringsDictionary, saved: saved)

>> I know that bridging between Swift types and Obj-C types was supposedly removed


It wasn't removed, in fact it was extended to a host of types that are now defined in the native Swift Foundation module (basically, everything that lost its NS prefix).


What was removed was implict bridging. Formerly the compiler would bridge whenever it thought appropriate. Now it will bridge only when you use "as" with a bridgeable type.


I would imagine that all of your results are deterministic and explainable, with sufficient knowledge of the compiler's internals, perhaps even reasonable. It just isn't what you'd expect.


Especially in Swift 3, I think it's highly desirable to create your Swift collections natively, then bridge them explicitly (if necessary) as a whole. In the sort of scenario you've been investigating, create a Dictionary, then do "(myDict as NSDictionary).write (…)". Other than trying to avoid fully rewriting code that started out in Obj-C, I can't see the use case for hybrid code of the sort you've shown.

>>What was removed was implict bridging.


That's what I meant, can't you read my mind? 🙂


Fair point about using native Swift collections and casting at the "end". However, after testing that doesn't resolve all the (what I consider to be) inconsistencies. For example, in the following the first write fails and the second succeeds.

let first = "First"
let second = "Second"
let third = "Third"

let aDate: Date? = Date()
let aBool = true
let aString: String? = "3"

var allSwiftDictionary: [String: Any] = [first : aDate, second : aBool, third : aString]
var saved = (allSwiftDictionary as NSDictionary).write(toFile: allSwiftDictionaryFileName, atomically: true)

allSwiftDictionary = [String: Any]()
allSwiftDictionary[first] = aDate
allSwiftDictionary[second] = aBool
allSwiftDictionary[third] = aString
saved = (allSwiftDictionary as NSDictionary).write(toFile: allSwiftDictionaryFileName, atomically: true)


The types of the values in the dictionary are still different: Optional<String>, Bool, and Optional<Date> in the first case and String, Bool, and Date in the second case. So (as you mentioned was a possibilty earlier) in the second case it is removing the optionality of the values when they are added to the dictionary. Which seems strange, because it is the same [String: Any] dictionary and the same optional variables being used as the values.

Implicit bridging aside, I think something more complicated is going on in Swift 3 than Swift 2, because bridged collection objects are now Any on the Swift side, whereas they used to be just AnyObject. Bridgeable dictionary keys are also a more general type than NSObject, now it's AnyHashable IIRC.


So your "dictionary as NSDictionary" used to mean something like "dictionary as! [NSObject: AnyObject] as NSDictionary" but now means "dictionary as! [AnyHashable: Any] as NSDictionary". That's not a strong enough condition for the NSDictionary contents to be plist compatible.


I think you'll actually need to do something like "dictionary as! [NSString: NSObject] as NSDictionary" to check both at compile time and run time that the correct bridging is done and the plist object conditions are met.


But this would be a good question to ask over at the swift.org mailing lists, since I may have gotten way off base. As well as the bug report.

Putting optional values into a dictionary is generally a recipe for confusion. There’s been some talk about making more changes in how this is bridged but a) I’m not sure if that’s going to go anywhere, and b) I’m not sure if it should. IMO it’s best to avoid this whole issue entirely.

You wrote:

The types of the values in the dictionary are still different:

Right. I think what’s going on here is that, in the first case:

var allSwiftDictionary: [String: Any] = [first : aDate, …]

the compiler see’s the

Any
and thus packs
Optional<Date>
into that, whereas the second case:
allSwiftDictionary = [String: Any]()
allSwiftDictionary[first] = aDate

acts more like a setter, where it’s natural to get optional promotion.

I have two suggestions:

  • If you’re looking for practical advice on how to deal with things currently, my recommendation is that you avoid this whole problem avoiding optionals. For example, if you write your first example as shown below, it produces the same result as the second.

  • If you’d like to discuss the theory behind this, and its future, hop on over to swift-evolution.

    var allSwiftDictionary: [String: Any] = [first : aDate!]

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
Swift 3: NSArray and NSDictionary, type inference, and writing to plists
 
 
Q