unable to access JSON array in swift - ios

I have a JSON file and I'm trying to access the array in it.
The JSON file looks like:
{
"cars": [{
"name": "BMW",
"icons": [["front.png", "back.png", "B3"],
["front_red", "back_red", "C4"]
]
}]
}
//cars is an array of dictionaries, I just mentioned one in the snippet.
I get the JSON data as:
func loadJSONData(){
if let path = Bundle.main.path(forResource: "testJSON", ofType: "json")
{
if let jsonData = NSData(contentsOfFile : path)
{
do {
if let jsonResult = try JSONSerialization.jsonObject(with: jsonData as Data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String:Any]
{
self.testJSONData = (jsonResult["cars"] as? Array)!
//self.testJSONData = (jsonResult["cars"] as? Array<Dictionary<String, Any>>)! //also tried this
}
}
catch let error as NSError {
print(error.localizedDescription)
}
}
}
}
testJSONData is declared as an array:
var testJSONData = [] as [Dictionary<String, Any>]
and the error occurs when trying to get the "icons" array from the JSON.
let namePredicate = NSPredicate(format: "name like BMW")
let filteredArray :Array = testJSONData.filter() { namePredicate.evaluate(with: $0) }
let carData: Dictionary = filteredArray[0] as Dictionary<String, Any>
let carIcons: Array = carData["icons"] as! Array //error at this line
Cannot convert value of type 'Array<_>' to specified type 'Array'
Can someone please show me where I'm doing wrong ? Thanks!

Array is a generic type in Swift, so when you want to declare an array variable, you always need to specific what type of elements the Array is holding. There's no such type as Array without specifying its Element type.
Also, there's no need for type annotations in Swift, the compiler can infer the types for you and you are explicitly telling the compiler the type by casting anyways.
carIcons should be of type Array<Array<String>> or as a shorthand [[String]]
let carIcons = carData["icons"] as! [[String]]
Some general comments about your code: don't use old Foundation types, such as NSData in Swift when they have native Swift equivalents. Also don't do force unwrapping of safe casted types, that makes no sense. Either handle the casting and unwrapping safely or simply force cast if you know the cast will succeed for sure. .mutableContainers have no effect in Swift, so don't use it. There's no need to cast error to NSError in a catch block, Swift has its own Error type.
func loadJSONData(){
if let fileURL = Bundle.main.url(forResource: "testJSON", withExtension: "json") {
do {
let jsonData = try Data(contentsOfFile: fileURL)
if let jsonResult = try JSONSerialization.jsonObject(with: jsonData) as? [String:Any], let cars = jsonResult["cars"] as? [[String:Any]] {
self.testJSONData = cars
} else {
print("Unexpected JSON format")
}
}
catch {
print(error)
}
}
}
However, if you are using Swift 4, the best solution would be using the type safe Codable protocol and JSONDecoder instead of JSONSerialization.

Related

How do I decode an object array and retrieve it from userdefaults?

I have an array with type [NotificationTriggers] that I would like to store in userdefaults. To do that, the data needs to be encoded and decoded. I have followed tutorials here:
https://cocoacasts.com/ud-5-how-to-store-a-custom-object-in-user-defaults-in-swift
and here:
https://www.hackingwithswift.com/example-code/system/how-to-load-and-save-a-struct-in-userdefaults-using-codable
But I still get an error that I can't seem to solve.
I have an extension of userDefaults where I do the magic in the get and set of the variable. NotificationTriggers Struct looks like this:
struct NotificationTriggers: Equatable, Codable {
var doorName: String
var notificationTrigger: String
}
Encoding seems to work, but in decoding I get an error saying
Cannot convert value of type '[Any]' to expected argument type 'Data'
This is the code:
extension UserDefaults {
var notificationTrigger: [NotificationTriggers] {
get {
if let data = self.array(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
do {
let decoder = JSONDecoder()
//CODE BELOW PRODUCE ERROR
if let decodedData = try decoder.decode([NotificationTriggers]?.self, from: data) {
return decodedData
}
} catch { }
}
return []
}
set {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(newValue)
self.setValue(data, forKey: UserDefaultsKey.notificationTrigger.rawValue)
} catch { }
}
}
}
I have tried casting the data:
UserDefaultsKey.notificationTrigger.rawValue) as? Data // get warning "Cast from '[Any]?' to unrelated type 'Data' always fails"
UserDefaultsKey.notificationTrigger.rawValue) as? [NotificationTriggers] // get error "Cannot convert value of type '[NotificationTriggers]' to expected argument type 'Data'"
Not sure what's missing here. Any ideas?
You save Data for the key UserDefaultsKey.notificationTrigger.rawValue with:
let encoder = JSONEncoder()
let data = try encoder.encode(newValue)
self.setValue(data, forKey: UserDefaultsKey.notificationTrigger.rawValue)
So the first mistake I see:
if let data = self.array(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
array(forKey:)? No, data(forKey:), you didn't save an Array, you saved a Data, a Data that might after some decoding "hides" an Array, but the system doesn't know it.
So, it should be:
if let data = self.data(forKey: UserDefaultsKey.notificationTrigger.rawValue) {
Then:
let decodedData = try decoder.decode([NotificationTriggers]?.self, from: data)
=>
let decodedData = try decoder.decode([NotificationTriggers].self, from: data)
Also, it's bad habit to have catch { }, if there is an error, you might want to know it:
catch {
print("Error while doingSomethingToCustomizeHere: \(error)")
}

Xcode Swift how do I print a value form my custom plist file?

Ok I have read so much about NSArray NSDictionary I'm lost now, what I want is to print the value 'name' from the first array item of my custom plist.
This is my plist:
and this is my code in my ViewController.swift:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let path = Bundle.main.path(forResource: "test", ofType: "plist")
let dic = NSArray(contentsOfFile: path!)
print(dic?.firstObject)
}
}
in my console I see:
Optional({
active = 0;
name = "John Doe";
})
I would think that print(dic?.firstObject["name"]) would do the trick
but I get an error: Value of type 'Any?' has no subscripts
So how do I print the values of name and active of my first array?
I know there are lots of answers on SO regarding this question, that's the reason I got so far.
but I just don't know how to fix this.
Kind regards,
Ralph
First of all please never use the NSArray/NSDictionary related API in Swift to read a property list. You are throwing away the type information.
However you can read the values with
let array = NSArray(contentsOfFile: path!) as! [[String:Any]]
for item in array {
let name = item["name"] as! String
let active = item["active"] as! Bool
print(name, active)
}
The dedicated and recommended API is PropertyListSerialization :
let url = Bundle.main.url(forResource: "test", withExtension: "plist")!
let data = try! Data(contentsOf: url)
let array = try! PropertyListSerialization.propertyList(from: data, format: nil) as! [[String:Any]]
A better way is the Codable protocol and PropertyListDecoder
struct User : Decodable {
let name : String
let active : Bool
}
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.url(forResource: "test", withExtension: "plist")!
let data = try! Data(contentsOf: url)
let array = try! PropertyListDecoder().decode([User].self, from: data)
for item in array {
print(item.name, item.active)
}
}
The code must not crash. If it does you made a design mistake
To use subscripts you first need to cast the object returned by
dic?firstObject
to a dictionary, you can also unwrap the optional at this point.
if let item = dic?firstObject as? [String: Any] {
print(item["name")
}

How can I read Plist in Swift?

I want read a NSArray form plist , and the code :
func loadPlistArray() -> [Any] {
var path: String? = Bundle.main.path(forResource:"MyCenter", ofType: "plist")
if let arry = NSArray(contentsOfFile: path!) {
return arry as! NSArray
}else{
return nil;
}
}
but always got errors below:
And After I got the data from plist, I fount that I can't see the Details of Dictionary :
And here is my plist:
should I add a generic in the array by var plistArray : [[String:Any]]?
The errors messages you are getting tell you what is wrong with your method, this is how I would write the function:
func loadPlistArray() -> [Any] { // 1
guard
let url = Bundle.main.url(forResource: "MyCenter", withExtension: "plist"), // 2
let list = NSArray(contentsOf: url) as? [Any] // 3
else { return [] } // 4
return list
}
And some commentary:
You are declaring the method to return an Array of Any items, but your method tries to return an NSArray.
It is recommended to use the URL based methods for accessing files, rather then the string based paths.
You have to use the Array methods to read the plist, but you can cast it to [Any]. However, if you know the type of items you have in the plist, I recommend that you return a properly type array from this method e.g. [String], [Int] etc.
You don't need to return an optional if the file can't be read. Depending on how you want to handle the error you could either return an empty array (as I've shown here) or convert your function into a throwing one so that if you can't read the file an error is thrown and can be handled by the calling code.
Your method signature clearly states that it returns an [Any] (i.e., Swift native Array containing elements of any type whatsoever), while you try to cast the return value into NSArray (even though it already is by virtue of intialization: NSArray(contentsOfFile:)).
Change it to:
return arry as? [Any]
// (will return nil if the cast fails - not a problem if you
// also apply the fix mentioned below...)
The other path tries to return nil; for that to be acceptable, your signature needs to be defined as returning an optional:
func loadPlistArray() -> [Any] // WRONG
func loadPlistArray() -> [Any]? // RIGHT
EDIT: If your app is structured in such a way that you can't afford to return nil from your method, you can instead return an empty array on failure:
else {
return [] // Empty array
}
(use [:] for empty dictionary)
Also, try to avoid using ! whenever possible, and switch to ? instead, unless you are 100% sure that whatever it is you are forcing will not fail and cause a runtime error (crash).
return (arry ) as! [Any]
You cannot return NSArray on type Any
I am using something like this in my project.
if let fileUrl = Bundle.main.url(forResource: "new", withExtension: "plist"),
let myDict = NSDictionary(contentsOf: fileUrl) as? [String:Any] {
print(myDict)
}
I have another plist for color which have array as a root.
if let fileUrl = Bundle.main.url(forResource: "color", withExtension: "plist"),
let data = try? Data(contentsOf: fileUrl) {
if let result = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [Any] {
print(result)
}
}

Conditional Binding Error in Swift

I am working on an example project (Written in an earlier version of Swift) in my efforts to learn Swift 2, and have run into a problem
I am getting a compile error with this -
class func loadMembersFromFile(path:String) -> [Member]
{
var members:[Member] = []
var error:NSError? = nil
if let data = NSData(contentsOfFile: path, options:[]),
json = NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary,
team = json["team"] as? [NSDictionary] {
for memberDictionary in team {
let member = Member(dictionary: memberDictionary)
members.append(member)
}
}
return members
}
The errors are:
Initializer for conditional binding must have Optional type, not 'NSData'
and
Call can throw, but it is not marked with 'try' and the error is not handled
My Swift programming experience is quite limited, so I have not been able to find a way to correct these errors. Any suggestions would be appreciated. Thanks.
Initializer for conditional binding must have Optional type, not
'NSData'
Means that you don't need conditional binding when result is not optional. But this is not true issue here. Because NSData initialiser can throw an error (which is stated in second error) and you may convert it to optional. Here is how your code will look:
class func loadMembersFromFile(path:String) -> [Member]
{
var members:[Member] = []
var error:NSError? = nil
if let data = try? NSData(contentsOfFile: path, options:[]),
json = (try? NSJSONSerialization.JSONObjectWithData(data, options: [])) as? NSDictionary,
team = json["team"] as? [NSDictionary] {
for memberDictionary in team {
let member = Member(dictionary: memberDictionary)
members.append(member)
}
}
return members
}

Swift 2 - NSJONSerialization.JSONObjectWithData with guard statement "Cannot convert value of type Element..."

I've started a Swift 2 project and I'm attempting to wrap my head around do, guard and throws. There are several other questions on Stack concerning this, but the issue I'm having is slightly different.
Here is my code:
enum JSONParsingError: String, ErrorType {
case URLCreationFailed = "Error: URL creation failed"
case SerializationFailed = "Error: JSON Parsing failed"
case DataDownloadingFailed = "Error: downloading data failed"
case DictionaryError = "Error: dictionary creation from JSON failed."
}
func fetchUserRepositories(urlString: String) throws {
do {
guard let reposURL = NSURL(string: urlString) else { throw JSONParsingError.URLCreationFailed }
guard let jsonData = NSData(contentsOfURL: reposURL) else { throw JSONParsingError.DataDownloadingFailed }
guard let jsonDictionary: NSDictionary = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .MutableContainers) as? NSDictionary else { throw JSONParsingError.SerializationFailed }
guard let reposArray = jsonDictionary["repos"] as? NSDictionary else { throw JSONParsingError.DictionaryError }
for repo in reposArray {
repositories.append(TestRepo(json: repo))
}
}
}
Regardless of how I cast jsonDictionary["repos"] I keep getting the same error in my for loop:
Cannot convert value of type 'Element' (aka '(key: AnyObject, value: AnyObject)') to expected argument type 'NSDictionary' (TestRepo is just a simple class that is initialized with a dictionary. Not the most ideal way, I know).
What am I missing?
reposArray is a dictionary, that is a collection of couples (key, value). In your code:
for repo in reposArray {
repositories.append(TestRepo(json: repo))
}
repo is bound to each element in turn. So, if TestRepo expects as parameter a dictionary, it receives instead an element (a couple (key, value)), and this is the cause of the error.

Resources