Get Values from JSON According to Condition - swift

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}

Related

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.
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.

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.

Creating and writing multiple lines of text to a text file (CSV)?

I'm trying to create a way for a user to sign in and store their username and password in a text file. This is for a school project, so it's not stored securely and I don't think I'll have time to set up/learn how to serialize the data anyways.
The main part I'm having issues with is trying to write the data as a CSV to later split the user's scores into an array based off of the data it received from the text file.
I've tried a different method of writing to the text file which was:
writeString.data(using: String.Encoding.utf8)?.write(to: fileURL, options: Data.WritingOptions.withoutOverwriting) But this didn't seem to work for me
struct UserAccount: Codable {
var username: String
var password: String
var scores: [Int]
}
var user = UserAccount(username: "", password: "", scores: [0])
func writeTextFile(_ user: UserAccount) {
//Creating text file to read and write user's data (username, password, and score values)
let fileName = "UserDataQuizApp"
let dir = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: "UserDataQuizApp") {
try fileManager.removeItem(atPath: fileName)
}
} catch {print("error when deleting \(fileName)") }
// If the directory was found, we write a file to it and read it back
if let fileURL = dir?.appendingPathComponent(fileName).appendingPathExtension("txt") {
print("The path is: \(fileURL)")
do {
try user.username.write(to: fileURL, atomically: false, encoding: .utf8)
try ",".write(to: fileURL, atomically: false, encoding: .utf8)
try user.password.write(to: fileURL, atomically: false, encoding: .utf8)
for i in 0 ... user.scores.count {
try ",".write(to: fileURL, atomically: true, encoding: .utf8)
try String(user.scores[i]).write(to: fileURL, atomically: true, encoding: .utf8)
}
} catch {
print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
}
}
}
Writing the array to the text file doesn't work at all right now because it says the index is out of range, but when I commented it out and tried to only write the username and password, only the password would be in the file when I checked it. I was expecting the username, a comma, and then the password to be in it
Reason for crash is in line for i in 0 ... user.scores.count it should be either for i in 0 ..< user.scores.count or for score in user.scores.
And you are trying to write each text to a file.
That means only last write(to:.. will have effect. Everything written will be overwritten.
To fix that, create a single string contains all info then write it to the file.
For eg the write function should be like :
func writeTextFile(_ user: UserAccount) {
//Creating text file to read and write user's data (username, password, and score values)
let fileName = "UserDataQuizApp"
let dir = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: "UserDataQuizApp") {
try fileManager.removeItem(atPath: fileName)
}
} catch {print("error when deleting \(fileName)") }
// If the directory was found, we write a file to it and read it back
if let fileURL = dir?.appendingPathComponent(fileName).appendingPathExtension("txt") {
print("The path is: \(fileURL)")
do {
var contents = [user.username, user.password]
contents.append(contentsOf: user.scores.map { "\($0)"})
let string = contents.joined(separator: ",")
print("contents \(string)")
try string.write(to: fileURL, atomically: false, encoding: .utf8)
} catch {
print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
}
}
}

Appending data into dictionary causes error in swift script

On trying to pretty print a valid JSON object I got stuck in an error that I am unable to circumvent. The problem started when I tried to append information into a dictionary and a extension for Dictionary was necessary for this task.
The aim of the code is to create an extension template for Albert launcher in Linux.
The error is:
./org.albert.extension.external.snippy.swift:10:21: warning: no calls to throwing functions occur within 'try' expression
let jsonS = try String(data: jsonD, encoding: String.Encoding.utf8)
^
fatal error: Error raised at top level: The operation could not be completed: file /home/buildnode/jenkins/workspace/oss-swift-4.0-package-linux-ubuntu-16_10/swift/stdlib/public/core/ErrorType.swift, line 187
Current stack trace:
0 libswiftCore.so 0x00007f089c352bc0 _swift_stdlib_reportFatalErrorInFile + 221
And the code is:
#! /usr/bin/swift
import Glibc
import Foundation
let albert_op = ProcessInfo.processInfo.environment["ALBERT_OP"]
extension Dictionary where Key == String {
func toPrettyJSON() throws -> String? {
let jsonD = try JSONSerialization.data(withJSONObject: self,options: [.prettyPrinted])
let jsonS = try String(data: jsonD, encoding: String.Encoding.utf8)
return jsonS
}
}
if albert_op == "METADATA" {
let metadata : [String: Any] = [
"iid": "org.albert.extension.external/v2.0",
"name": "snippets",
"version": "0.1",
"author": "lf-araujo",
"dependencies": [],
"trigger": "snip "
]
let jsonData = try JSONSerialization.data(withJSONObject: metadata)
let JSONString = String(data: jsonData, encoding: String.Encoding.utf8)!
print(JSONString)
} else if albert_op == "QUERY" {
let filemgr = FileManager.default
let filelist = try filemgr.contentsOfDirectory(atPath: "~/.snippy")
func buildItem(name: String) -> [String:Any] {
let action : [String: Any] = [
"name": name
]
return action
}
var items : [String: Any] = [:]
items["items"] = filelist.map { buildItem(name: $0) }
if let jsonStr = try? items.toPrettyJSON() {
print(jsonStr!)
}
}
exit(0)
In order to reproduce the problem, one needs to run the code with: ALBERT_OP="QUERY" ./script.swift.
What am I doing wrong in this particular case? Is it related to the fact that I am running it as a script?
I think your issue is at try filemgr.contentsOfDirectory(atPath: "~/.snippy"), specifically the fact that Swift doesn't automatically expand tildes in paths. You can manually do it with ("~/.snippy" as NSString).expandingTildeInPath

Alamofire responseArray String array for keyPath

I have a rest call which returns array of string [String] for keyPath "data", for example...
{
"data": [
"1",
"3",
"5",
"2",
"4",
"9"
]
}
I am trying to get it via responseArray(keyPath: "data"), but get compile error for line *.responseArray(keyPath: "data") {(response: DataResponse<[String]>) in*
Cannot convert value of type '(DataResponse<[String]>) -> ()' to expected argument type '(DataResponse<[_]>) -> Void'
Part of request example
alamofireManager.request(url)
.responseArray(keyPath: "data") {(response: DataResponse<[String]>) in
if response.result.isSuccess {
if let data = response.result.value {
//process success
} else {
// handle error
}
}
...
Does anybody of you know how to do it ?
The problem is that String isn't Mappable. As per https://github.com/Hearst-DD/ObjectMapper/issues/487, these are the proposed solutions:
In this situation I would recommend accessing the data directly from the Alamofire response. You should be able to simply cast it to [String].
Alternatively, you may be able to subclass String and make the subclass Mappable, however I think that is more work than necessary for the your situation
Using Swift's 4 Codable (no external dependencies):
struct APIResponse: Decodable {
let data: [String]
}
let url = "https://api.myjson.com/bins/1cm14l"
Alamofire.request(url).responseData { (response) in
if response.result.isSuccess {
if let jsonData = response.result.value,
let values = try? JSONDecoder().decode(APIResponse.self, from: jsonData).data {
//process success
print(values)
} else {
// handle error
}
}
}