Using Decodable and JSONs - ios

I have a JSON file that is formatted in the following way:
[
{
"QID": "B002",
"Stash": "Basic",
"Category": "Geography",
"Question": "What is the highest mountain on earth?",
"Answer": "Mt Everest"
},
{
"QID": "B003",
"Stash": "Basic",
"Category": "General",
"Question": "What is the gestation period for a pregnant elephant?",
"Answer": "2 years"
}
]
I'm trying to make a structure so that I can load all the questions in my JSON file into my quiz-app. So far from what I've researched from JSON and the new "Decodable" thing apple added I have my Swift code as follows (Note there's a failed attempt commented out):
var STASHES_SELECTED = ["BasicStash", "MediumStash", "HardStash"]
struct TriviaQuestion: Decodable {
let QID: String
let Stash: String
let Categoroy: String
let Question: String
let Answer: String
}
func loadQuestionStash()
{
/*
for var i in STASHES_SELECTED
{
let url = Bundle.main.url(forResource: STASHES_SELECTED[i], withExtension: "JSON") //CANT GET THIS TO WORK!, SAYS CANNOT SUBSCRIPT TYPE [STRING] WITH INDEX TYPE 'STRING'
}*/
if let url = Bundle.main.url(forResource: "BasicStash", withExtension: "JSON")
{
let json = try? Data(contentsOf: url)
let questions = try? JSONDecoder().decode(TriviaQuestion.self, from: json!)
print (questions!) //FATAL ERROR, FOUND NIL
}
}
As you can see from the code comments, that currently gives me a fatal error "found nil while unwrapping". So I assume that the previous line JSONDecoder(). failed horribly.
I am not sure if I am doing this correctly as it's my firs time working with JSONs and I've just been pretty much cookie-cutter following tutorials and posts online. I'd really appreciate some help here. Also the .self after TriviaQuestion was added by the system (I think the problem might be somewhere in there)

As #Oxthor mentioned the typing error,
I just want to add that always use quicktype.io to create your data struct. You will avoid typos and save your time:
// To parse the JSON, add this file to your project and do:
//
// let triviaQuestion = Array.from(json: jsonString)!
import Foundation
struct TriviaQuestion: Codable {
let answer: String
let category: String
let qID: String
let question: String
let stash: String
}
// MARK: Top-level extensions -
extension Array where Element == TriviaQuestion {
static func from(json: String, using encoding: String.Encoding = .utf8) -> [PurpleTriviaQuestion]? {
guard let data = json.data(using: encoding) else { return nil }
return from(data: data)
}
static func from(data: Data) -> [TriviaQuestion]? {
let decoder = JSONDecoder()
return try? decoder.decode([TriviaQuestion].self, from: data)
}
static func from(url urlString: String) -> [TriviaQuestion]? {
guard let url = URL(string: urlString) else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
return from(data: data)
}
var jsonData: Data? {
let encoder = JSONEncoder()
return try? encoder.encode(self)
}
var jsonString: String? {
guard let data = self.jsonData else { return nil }
return String(data: data, encoding: .utf8)
}
}
// MARK: Codable extensions -
extension TriviaQuestion {
enum CodingKeys: String, CodingKey {
case answer = "Answer"
case category = "Category"
case qID = "QID"
case question = "Question"
case stash = "Stash"
}
}

You mistyped the category attribute in the TriviaQuestion struct. You have categoroy but it should be category.

Related

Swift Dictionary API "No value associated with key CodingKeys(stringValue: \"type\", intValue: nil) (\"type\").", underlyingError: nil))

I am running into a error showing that there is no value for "type" from the API I'm trying to grab from. Ive tried looking through other posts revolving around this but can't find anything that works for me without causing a different issue to come up.
The full error I'm getting is "No value associated with key CodingKeys(stringValue: "type", intValue: nil) ("type").", underlyingError: nil))
The API I'm trying to grab from is: https://github.com/ToontownRewritten/api-doc/blob/master/invasions.md
import UIKit
struct InvasionList: Decodable {
let type: String
let asOf: Int?
let progress: String?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://www.toontownrewritten.com/api/invasions"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return }
do {
let invasions = try JSONDecoder().decode(InvasionList.self, from: data)
print(invasions.type)
} catch let err {
print(err)
}
}.resume()
}
}
Your InvasionList doesn't actually match the structure of the JSON that you're trying to decode. It matches one small part of it, but you need to decode the entire thing. This includes the outermost wrapper (the part with lastUpdated and the dynamic keys for each "invasion").
Here's a working example:
let jsonData = """
{"lastUpdated":1624230138,"invasions":{"Gulp Gulch":{"asOf":1624230127,"type":"Robber Baron","progress":"3797/7340"},"Splashport":{"asOf":1624230129,"type":"Ambulance Chaser","progress":"2551/8000"},"Kaboom Cliffs":{"asOf":1624230131,"type":"Money Bags","progress":"5504/6000"},"Boingbury":{"asOf":1624230132,"type":"Tightwad","progress":"4741/8580"}},"error":null}
""".data(using: .utf8)!
struct InvasionWrapper: Decodable {
let lastUpdated : Int
let invasions : [String:Invasion]
}
struct Invasion: Decodable {
let type: String
let asOf: Int?
let progress: String?
}
do {
let list = try JSONDecoder().decode(InvasionWrapper.self, from: jsonData)
print(list)
} catch {
print(error)
}

Why am not able to access my model class in Swift Project?

How to access my Model from ViewController and use the Model data to load in table view????
Source Code Link
My ViewController looks like this
import UIKit
class ViewController: UIViewController {
var cclm: CountryCodeListModel?
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(hello), userInfo: nil, repeats: true)
readLocalJSONFile(forName: "countryList")
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
}
#objc func hello()
{
print(cclm?.data?[0].flag)
}
}
and my model class look like this
struct CountryCodeList : Decodable {
var alpha2Code: String?
var alpha3Code: String?
var flag : String?
var name : String?
var code : String?
}
public struct CountryCodeListModel : Decodable {
var data : [CountryCodeList]?
}
var cclm: CountryCodeListModel?
//Method to load json
func readLocalJSONFile(forName name: String) {
do {
if let filePath = Bundle.main.path(forResource: name, ofType: "json") {
let fileUrl = URL(fileURLWithPath: filePath)
let data = try Data(contentsOf: fileUrl)
if let countryCodeObject = parse(jsonData: data) {
cclm = countryCodeObject
print(cclm?.data?[1].alpha2Code ?? "") //Printing Correct Value
}
}
} catch {
print("error: \(error)")
}
}
func parse(jsonData: Data) -> CountryCodeListModel?{
var dataArray : [Dictionary<String,Any>] = [[:]]
var country = Dictionary<String,Any>()
var modelData = Dictionary<String,Any>()
do {
// make sure this JSON is in the format we expect
if let json = try JSONSerialization.jsonObject(with: jsonData, options: []) as? Dictionary<String,Any> {
dataArray.removeAll()
for item in json["data"] as! [Dictionary<String, Any>] {
country = item
let url = URL(string: country["flag"] as? String ?? "")
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
let image = UIImage(data: data!)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileName = url?.lastPathComponent // name of the image to be saved
let fileURL = documentsDirectory.appendingPathComponent(fileName ?? "")
if let data = image?.jpegData(compressionQuality: 1.0){
do {
try data.write(to: fileURL)
country["flag"] = fileURL.absoluteString
//print("file saved")
//urlAsString = fileURL.absoluteString
} catch {
print("error saving file:", error)
}
}
dataArray.append(country)
country.removeAll()
}
modelData["data"] = dataArray
//print(modelData)
let jsonData1 = try JSONSerialization.data(withJSONObject: modelData, options: [])
do {
let decodedData = try JSONDecoder().decode(CountryCodeListModel.self, from: jsonData1)
return decodedData
} catch {
print("error: \(error)")
}
}
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
return nil
}
Problem statement:
Iam reading local json and take the url value of flag key and download corresponding images to local. Once i download then am taking the localpath and update in the dictionary and then create JSON object and update my model class.
Now, am trying to access my model class from ViewController like below
print(CountryCodeListModel?.data?[0].name) //check screenshot for error
print(cclm?.data?[0].flag) // this prints nil always
Please check the error screenshots attached2
My JSON look like this
{
"meta":{
"success":true,
"message":"Successfully retrieved country details",
"code":"200"
},
"data":[
{
"alpha2Code":"AF",
"alpha3Code":"AFG",
"flag":"https://raw.githubusercontent.com/DevTides/countries/master/afg.png",
"name":"Afghanistan",
"code":"+93"
},
{
"alpha2Code":"AX",
"alpha3Code":"ALA",
"flag":"https://raw.githubusercontent.com/DevTides/countries/master/ala.png",
"name":"Aland Islands",
"code":"+358"
},
{
"alpha2Code":"AL",
"alpha3Code":"ALB",
"flag":"https://raw.githubusercontent.com/DevTides/countries/master/alb.png",
"name":"Albania",
"code":"+355"
},
{
"alpha2Code":"DZ",
"alpha3Code":"DZA",
"flag":"https://raw.githubusercontent.com/DevTides/countries/master/dza.png",
"name":"Algeria",
"code":"+213"
},
{
"alpha2Code":"AS",
"alpha3Code":"ASM",
"flag":"https://raw.githubusercontent.com/DevTides/countries/master/asm.png",
"name":"American Samoa",
"code":"+1684"
}
]
}
You are trying to decode something that doesn't exist.
print(CountryCodeListModel?.data?[0].name) //check screenshot for error
print(cclm?.data?[0].flag) // this prints nil always
The above code states that you want:
the name of
the variable data at position 0 of
the struct CountryCodeListModel.
What you want to do is:
the name of
the variable at position 0 of
an INSTANCE of the struct CountryCodeListModel.
For example...
func readLocalJSONFile(forName name: String) {
do {
if let filePath = Bundle.main.path(forResource: name, ofType: "json") {
let fileUrl = URL(fileURLWithPath: filePath)
let data = try Data(contentsOf: fileUrl)
if let countryCodeObject = parse(jsonData: data) {
cclm = countryCodeObject
print(cclm?.data?[1].alpha2Code ?? "") //Printing Correct Value
print(cclm?.data?[0].flag ?? "")
print(countryCodeObject?.data[0].flag ?? "") // Same as the line above
}
}
} catch {
print("error: \(error)")
}
}
Unless you are trying to use a static variable (at which you would use CountryCodeListModel.data), you need to make sure you are actually using an instance of the structure or an object of a class to reference your properties.
CAVEAT
CountryCodeListModel is a structure. CountryCodeListModel() is an instance of the structure CountryCodeListModel. Since you can have multiple instances of a structure, you need to reference a specific structure when accessing data. Thus, CountryCodeListModel.data will not work and it needs to be CountryCodeListModel().data. In this case, you have cclm.data.

Decoding Nested Json, Missing objects using SwiftyJson iOS

Im calling this api to receive single rocket launch event:
https://launchlibrary.net/1.4/launch/next/1 using simple Get request.
Trying to decode using SwiftyJson (also tried Codable) with lack of success to read the "rocket" -> "imageURL"
here is my code:
struct LaunchHistory {
var launches = [LaunchItem]()
init(with json:JSON) {
for launch in json["launches"].arrayValue {
let launchItem = LaunchItem(with: launch)
launches.append(launchItem)
}
}
}
struct LaunchItem {
let id:Int?
let name: String?
let tbddate: Int?
let status: LaunchStatus?
let rocketImage: String?
init(with json:JSON) {
self.id = json["id"].int
self.name = json["name"].string
self.tbddate = json["tbddate"].int
self.status = LaunchStatus(rawValue: json["status"].int ?? 0)
self.rocketImage = json["rocket"]["imageURL"].string
}
}
when LaunchItem decoded, all i 11 properties/key instead of almost double.
the rocket object is missing.
what am i missing here?
thanks!
It's pretty easy with (De)Codable
struct Root : Decodable {
let launches : [LaunchItem]
}
struct LaunchItem : Decodable {
let id: Int
let name: String
let tbddate: Int
let rocket: Rocket
}
struct Rocket : Decodable {
let imageURL : URL
}
let url = URL(string: "https://launchlibrary.net/1.4/launch/next/1")!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode(Root.self, from: data!)
print(result.launches.first?.rocket.imageURL ?? "n/a")
} catch {
print(error)
}
}
task.resume()

How to sort JSON in swift based on persian characters

i am new to swift , i going to sort a json file in bundle not by code , is there anyway to sort the file using by code or not , i want to sort it from "citname":"لومار"
the json file is :
{
"data":[
{
"_id":1,
"citproidint":4,
"citname":"لومار"
},
{
"_id":2,
"citproidint":4,
"citname":"ايوان"
},
{
"_id":3,
"citproidint":12,
"citname":"آبعلی"
},
{
"_id":4,
"citproidint":25,
"citname":"نيشابور"
},
{
"_id":5,
"citproidint":27,
"citname":"سقز"
},
]
}
... // 827 id is in this json file
and for every block in this json file i parse like this , and everything is fine but i want to use it sorted in my pickerview then i need to save sorted modals
guard let path = Bundle.main.path(forResource: "citybig", ofType: "json") else { return }
// city Text file:// ...
let url = URL(fileURLWithPath: path)
do {
// data explained in bytes
let data = try Data(contentsOf: url)
// we get the json file text
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
// put json in nsdictinary so we can access each index and person sooo ... id and more
for (key,value) in json as! NSDictionary {
if (key as! String) == "data" {
let value2 = value.s
for each in value as! [[String: Any]] {
let singleCityModal = cityModal()
for (key,value) in each {
switch key {
case "_id" :
singleCityModal.id = value as? Int
case "citproidint" :
singleCityModal.citproidint = value as? Int
case "citname" :
singleCityModal.citname = value as? String
default:
break
}
}
cityFirstModal.append(singleCityModal)
}
cityFinalModal.append(contentsOf: cityFirstModal)
Your way to parse the data is quite strange, in Swift 4 there is a more convenient way, the Decodable protocol.
You need two structs:
struct CityData : Decodable {
private enum CodingKeys: String, CodingKey { case cities = "data"}
let cities : [City]
}
struct City : Decodable {
private enum CodingKeys: String, CodingKey { case id = "_id", citproidint, name = "citname"}
let id, citproidint: Int
let name : String
}
and the code to parse the JSON becomes a bit shorter (and more efficient)
let url = Bundle.main.url(forResource: "citybig", withExtension: "json")!
let data = try! Data(contentsOf: url)
let cityData = try! JSONDecoder().decode(CityData.self, from: data)
The exclamation marks are intended. As the (immutable) file is in the bundle the code must not crash. If it does you made a design mistake.
It's just one additional line to sort the cities by citproidint
let sortedCities = cityData.cities.sorted(by: {$0.citproidint < $1.citproidint})
I'm not familiar with Farsi but if you want to sort by name the selector localizedStandardCompare might do the job
let sortedCities = cityData.cities.sorted(by: {$0.name.localizedStandardCompare($1.name) == .orderedAscending })

How to save custom objects that implements Codable

It's now easier with Swift 4 to encode / decode to and from JSON or Properties list.
But I can't find how to encode to Data using Codable, without using Objective-C methods initWithCoder and encodeWithCoder.
Considering this simple class:
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
How can I encode it to Data using CodingKeys and not initWithCoder and encodeWithCoder?
EDIT:
I also need to be able to deserialize objects previously saved in userdefaults using NSKeyedArchiver.
Well, you no longer need NSKeyedArchiver.
Try this:
let questionObj = Question(title: "WWDC, 2017", answer: 1,question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
UserDefaults.standard.set(encoded, forKey: "K_Question")
}
let decoder = JSONDecoder()
if let questionData = UserDefaults.standard.data(forKey: "K_Question"),
let question = try? decoder.decode(Question.self, from: questionData) {
print(question.title)
print(question.answer)
print(question.question)
}
Swift 5: a great simple extension for UserDefaults:
extension UserDefaults {
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.standard.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.standard.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
Usage
save
UserDefaults.standard.save(currentUser, forKey: "currentUser")
get
let user: User? = UserDefaults.standard.getObject(forKey: "currentUser")
Well it can be achieved via JSONEncoder and JSONDecoder.
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
let questionObj = Question(title: "Swift", answer: "Open Source",question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Question.self, from: encoded) {
print(decoded)
}
}
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
class UserDefaults_Question {
static let key = "myapp.trick.question"
static var value: UserDefaults_Question? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else {
print("no model for key: \(key)")
return nil
}
guard let model = try? JSONDecoder().decode(UserDefaults_Question.self, from: data) else {
print("failed to decode model for key: \(key)")
return nil
}
print("did load model for key: \(key)")
return model
}
set {
guard let value = newValue, let data: Data = try? JSONEncoder().encode(value) else {
print("removing model for key: \(key)")
UserDefaults.standard.removeObject(forKey: key)
return
}
print("inserting model for key: \(key)")
UserDefaults.standard.set(data, forKey: key)
}
}
}
UserDefaults_Question.value = Question(title: "Next President", answer: 666, question: -1)

Resources