Search code examples
swiftnsdictionaryplistswift5

How can I store an NSDictionary in a String:String?


I'm trying to load a plist (keys are unique words, values are their English-language definitions) into a dictionary. I can do a one-off like this:

let definitionsFile = URL(fileURLWithPath: Bundle.main.path(forResource: "word_definitions", ofType:"plist")!)
let contents = NSDictionary(contentsOf: definitionsFile)
guard let value = contents!.object(forKey: lastGuess) as? String else {
      print("value from key fail")
      return
} 

...but it has to load the file every time I use it. So I tried moving the code to the program loader and storing the data in the definitions dictionary (the capitalized message is the problem area):

let definitionsFile = URL(fileURLWithPath: Bundle.main.path(forResource: "word_definitions", ofType:"plist")!)
if let contents = NSDictionary(contentsOf: definitionsFile) as? [String : String] {
    print("loaded definitions dictionary")
    if case state.definitions = contents {
        print("added definitions to state")
    } else {
        print("FAILED TO ADD DEFINITIONS TO STATE")
    }
} else {
    print("failed to load definitions dictionary")
}

It's failing at the point where I assign it to state.definitions (which is a String:String dictionary). Is there something I'm missing? Do I need to change state.definitions to String:Any and rewrite every access?

UPDATE: Based on Tom Harrington's comment, I tried explicitly creating state.definitions as a NSDictionary (removing the as [String:String] bit) and it's still not storing the dictionary.


Solution

  • I put your code in a Playground that both generates a plist file and then uses NSDictionary to parse it out. Here is the full playground

    import Foundation
    
    let stringPairs = [
        "One" : "For the Money",
        "Two" : "For the Show",
        "Three" : "To Get Ready",
        "Four" : "To Go",
    ]
    
    let tempDirURL = FileManager.default.url(for: .itemReplacementDirectory,
                                          in: .userDomainMask,
                                          appropriateFor: Bundle.main.bundleURL,
                                          create: true)
    let demoFileURL = tempDirURL.appendingPathComponent("demo_plist.plist")
    do {
        if let plistData = try? PropertyListSerialization.data(
            fromPropertyList: stringPairs,
            format: .xml,
            options: 0) {
            try plistData.write(to: demoFileURL)
        }
    } catch {
        print("Serializing the data failed")
    }
    
    struct State {
        var definitions: [String: String]
    }
    
    var state = State(definitions: [:])
    
    if let fileContent = NSDictionary(contentsOf: demoFileURL),
       let contents = fileContent as? [String : String] {
        print("loaded definitions dictionary")
        state.definitions = contents
    } else {
        print("failed to load definitions dictionary")
    }
    
    debugPrint(state.definitions)
    

    Note I just made up something for the state variable and its type.

    It seems to work just fine and prints:

    loaded definitions dictionary
    ["Four": "To Go", "Two": "For the Show", "One": "For the Money", "Three": "To Get Ready"]
    

    One thing I changed was your if case ... statement. I'm entirely sure what this construct means in this context. The Swift Language Guide says an if case should be followed by a pattern and an initializer. In my code "state.definitions" is not a pattern so the if case always returns false. But it seems to me that this should be some kind of compiler error.

    At any rate, by pulling the binding of contents into its own clause of the outer if I can be sure that by the time I get into the if that contents is not null.