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

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.

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.

Related

Get Values from JSON According to Condition

I have a local Json file in app bundle.
Existing Json file:
[
{
"id": 1001,
"key1": true,
"key2": "key2Value",
},
{
"id": 1002,
"key1": false,
"key2": "key2Value",
},
{
"id": 1003,
"key1": true,
"key2": "key2Value",
},
]
I want to loop over the json and get the ids if key1 value is true.
I tried like this but I got error:
let vehicleInfoTest: [Vehicle] = readJSONTest("vehicleInfo.json")
func readJSONTest<T: Codable>(_ named: String) -> T{
let data: Data
let ids: Int
do {
let fileURL = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("vehicleInfo.json")
data = try Data(contentsOf: fileURL)
// I can successfully get the foo
let foo = try JSONDecoder().decode(T.self, from: data)
// For loop gives an error: For-in loop requires 'Vehicle' to conform to 'Sequence'
for item in foo as! Vehicle{
if item.key1 == true{
ids = item.id
}
}
return foo
} catch {
fatalError("Couldn't find in main bundle.")
}
}
Finally ids should be like this:
ids = 1001, 1003
How can I get the ids?
Your code is a bit of a mess, you have a generic function that you try to use as a non-generic function and you have also some other messy stuff in it. Since you first need to call the generic function as step 1 and then do the filtering of the result in a second step the following code will give you the Id's where key1 is true
let vehicleInfoTest: [Vehicle] = readJSONTest("vehicleInfo.json")
let idValues = vehicleInfoTest.filter({ $0.key1 }).map( {$0.id })
Now to get this to work the function needs to be cleaned up
func readJSONTest<T: Codable>(_ named: String) -> T{
do {
let fileURL = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(named). //use parameter, not hardcoded string
let data = try Data(contentsOf: fileURL)
return try JSONDecoder().decode(T.self, from: data) // directly return decoded values
} catch {
fatalError("Decoding failed with error \(error)") // More useful error message
}
}
A simple
let ids = vehicles.filter { $0.key1 }.map { $0.id }
instead of your for loop should do the trick - however, this should not be part of the generic method.
Either readJSONTest is generic, or it knows about the internals of Vehicle, but it really shouldn't do both.
Also, please don't say x.key1 == true when key1 is already a non-optional Bool.
The non-generic code in the generic function makes no sense.
The loop presumes that T is an array and the type has a member key1. Filter the array outside the loop and make the function throw
func readJSONTest<T: Decodable>(_ named: String) throws -> T {
let fileURL = try FileManager.default
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("vehicleInfo.json")
let data = try Data(contentsOf: fileURL)
return try JSONDecoder().decode(T.self, from: data)
}
let vehicleInfoTest: [Vehicle] = try readJSONTest("vehicleInfo.json")
let ids = vehicleInfoTest.lazy.filter{$0.key1}.map{$0.id}

How to handle dynamic keys with single element as value in Swift

I'm having a problem figuring out how to handle dynamic keys with a single value one level deep. I've seen a few examples but they appear to handle other cases.
Here's an example.
[
{ "asdofjiodi": "asdofidj.com" },
{ "sadjlkj": "iejjol.com" },
{ "ijijwjljlijl": "adsijf.com" },
{ "jgncmkz": "mlkjaoijf.com" }
]
Any ideas on how I accomplish this with Codable or CodingKey? This is what I'd like for the end result.
["asdofidj.com", "iejjol.com", "adsijf.com", "mlkjaoijf.com"]
let url = Bundle.main.url(forResource: "file", withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
// NOTE: The below line doesn't work because I'm not sure how to do the encoding/decoding
try? decoder.decode([[String: String]].self, from: data)
First of all, is not a valid JSON. A "Valid JSON" can be a JSON Array (multiple json objects) or a single JSON object (starts and ends with { and })
After cleaning this...
You are trying to make a dictionary (json object) from a data. You don't need JSONDecoder to accomplish this. Try using JSONSerialization with jsonObject static function...
let data = Data("{\"asdofjiodi\": \"asdofidj.com\",\"sadjlkj\": \"iejjol.com\",\"ijijwjljlijl\": \"adsijf.com\",\"jgncmkz\": \"mlkjaoijf.com\"}".utf8)
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
let values = json?.map({ $1 })
print(values)
I Hope I helped!
JSONSerialization Apple documentation
Here is a minimal working example, given the JSON in your question:
let json = """
[
{ "asdofjiodi": "asdofidj.com" },
{ "sadjlkj": "iejjol.com" },
{ "ijijwjljlijl": "adsijf.com" },
{ "jgncmkz": "mlkjaoijf.com" }
]
"""
let data = Data(json.utf8)
let decoder = JSONDecoder()
do {
let decodedJson = try decoder.decode([[String: String]].self, from: data)
let values = decodedJson.compactMap(\.values.first)
print(values)
} catch {
print("Error: \(error.localizedDescription)")
}
If this doesn't appear to work for you, it may be related to how you load in the JSON.

Making a variable from if statement global

While encoding JSON, I´m unwrapping stuff with an if let statement, but I'd like to make a variable globally available
do {
if
let json = try JSONSerialization.jsonObject(with: data) as? [String: String],
let jsonIsExistant = json["isExistant"]
{
// Here I would like to make jsonIsExistant globally available
}
Is this even possible? If it isn't, I could make an if statement inside of this one, but I don't think that would be clever or even possible.
delclare jsonIsExistant at the place you want it. If you are making an iOS App, than above viewDidLoad() create the variable
var jsonIsExistant: String?
then at this point use it
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: String],
let tempJsonIsExistant = json["isExistant"] {
jsonIsExistant = tempJsonIsExistant
}
}
This could be rewritten like so though
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: String] {
jsonIsExistant = json["isExistant"]
}
} catch {
//handle error
}
If handled the second way, then you have to check if jsonIsExistant is nil before use, or you could unwrap it immediately with a ! if you are sure it will always have a field "isExistant" every time that it succeeds at becoming json.
It doesn't make sense to expose a variable to the outside of an if let statement:
if let json = ... {
//This code will only run if json is non-nil.
//That means json is guaranteed to be non-nil here.
}
//This code will run whether or not json is nil.
//There is not a guarantee json is non-nil.
You have a few other options, depending on what you want to do:
You can put the rest of the code that needs json inside of the if. You said you didn't know if nested if statements are "clever or even possible." They're possible, and programmers use them quite often. You also could extract it into another function:
func doStuff(json: String) {
//do stuff with json
}
//...
if let json = ... {
doStuff(json: json)
}
If you know that JSON shouldn't ever be nil, you can force-unwrap it with !:
let json = ...!
You can make the variable global using a guard statement. The code inside of the guard will only run if json is nil. The body of a guard statement must exit the enclosing scope, for example by throwing an error, by returning from the function, or with a labeled break:
//throw an error
do {
guard let json = ... else {
throw SomeError
}
//do stuff with json -- it's guaranteed to be non-nil here.
}
//return from the function
guard let json = ... else {
return
}
//do stuff with json -- it's guaranteed to be non-nil here.
//labeled break
doStuff: do {
guard let json = ... else {
break doStuff
}
//do stuff with json -- it's guaranteed to be non-nil here.
}

Handling Swift Dicts: fatal error: can't unsafeBitCast between types of different sizes

I have a function in Swift that needs to be able to handle multiple types. Specifically, it needs to be able to parse both Dicts and Strings.
The problem I have is the Dicts could be several types, depending on their origin. So I could be provided with [String:Any] or [String:String] (coming from Swift) or [String:AnyObject] (coming from objc). The top level parsing function takes Any, which it then tests for specific types and attempts to parse them.
At first I just tried testing for if let dict = object as? [String:Any], but if I passed in another type [String:AnyObject] or [String:String] it failed. So I tried testing each type:
func parseLink(object: Any) {
if let dict = object as? [String:Any] {
return self.parseDict(dict)
} else if let dict = object as? [String:AnyObject] {
return self.parseDict(dict)
} else if let dict = object as? [String:String] {
return self.parseDict(dict)
} else if let string = object as? String {
return parseURL(string)
}
}
func parseDict(dict: [String:Any]) { ..... }
So I've created some Unit Tests to test the behavior:
func testDictTypes() {
let testDict: [String:Any] = [ "orgId" : "123456789" ]
let link = OrgContextLinkParser().parseLink(testDict)
XCTAssertNotNil(link)
let testDict1: [String:AnyObject] = [ "orgId" : "123456789" ]
let link2 = OrgContextLinkParser().parseLink(testDict1)
XCTAssertNotNil(link2)
let testDict3: [String:String] = [ "orgId" : "123456789" ]
let link3 = OrgContextLinkParser().parseLink(testDict3)
XCTAssertNotNil(link3)
}
This all compiles fine, but I get a fatal runtime error if a [String:AnyObject] is passed in. This is troubling since Swift's type system is supposed to prevent these kind of errors and I get no warning or errors thrown when I compile.
I also really don't want to duplicate the exact same logic multiple times just to handle different dict types. I.E., handling [String:Any], [String:AnyObject] and [String:String] have virtually the exact same logic.
The only possible solution I've seen is to actually duplicate the dictionary, which seems rather expensive (Convert [String: AnyObject] to [String: Any]). For performance reasons, it seems better to just copy paste the code and change the function signatures... but really!? That's seems excessive.
The best solution seems to be to parse a Dict as [String:AnyObject] and copy the value only if it's [String:Any]:
if let dict = object as? [String:Any] {
var objDict: [String:AnyObject] = [:]
for (key, value) in dict {
if let obj = value as? AnyObject {
objDict[key] = obj
}
}
return self.parseDict(objDict)
I don't particularly like this, but so far it's be best I've been able to come up with.
Does anyone have any idea how to handle this properly? I'm especially concerned that I can cast Any as [String:AnyObject], pass it to a function that takes [String:Any] and I get no compiler errors, even though it crashes at runtime.

Swift: How to convert string to dictionary

I have a dictionary which i convert to a string to store it in a database.
var Dictionary =
[
"Example 1" : "1",
"Example 2" : "2",
"Example 3" : "3"
]
And i use the
Dictionary.description
to get the string.
I can store this in a database perfectly but when i read it back, obviously its a string.
"[Example 2: 2, Example 3: 3, Example 1: 1]"
I want to convert it back to i can assess it like
Dictionary["Example 2"]
How do i go about doing that?
Thanks
What the description text is isn't guaranteed to be stable across SDK versions so I wouldn't rely on it.
Your best bet is to use JSON as the intermediate format with NSJSONSerialization. Convert from dictionary to JSON string and back.
I created a static function in a string helper class which you can then call.
static func convertStringToDictionary(json: String) -> [String: AnyObject]? {
if let data = json.dataUsingEncoding(NSUTF8StringEncoding) {
var error: NSError?
let json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &error) as? [String: AnyObject]
if let error = error {
println(error)
}
return json
}
return nil
}
Then you can call it like this
if let dict = StringHelper.convertStringToDictionary(string) {
//do something with dict
}
this is exactly what I am doing right now. Considering #gregheo saying "description.text is not guaranteed to be stable across SKD version" description.text could change in format-writing so its not very wise to rely on.
I believe this is the standard of doing it
let data = your dictionary
let thisJSON = try NSJSONSerialization.dataWithJSONObject(data, options: .PrettyPrinted)
let datastring:String = String(data: thisJSON, encoding: NSUTF8StringEncoding)!
you can save the datastring to coredata.