Project files:
https://jumpshare.com/v/Otai3BBXYwfvyz8jb53k
(Would be wise to view these to see structure of project)
Problem:
Ok, so i'm following a tutorial that creates a UITableView with headers and then cell content.
The code worked and runs fine, Now I want to extend beyond that tutorial and dynamically load that content using alamofire and SwiftyJSON.
In the tutorial, the code used is like so:
func getSectionsFromData() -> [Sections] {
var sectionsArray = [Sections]()
let animals = Sections(title: "Animals", objects: ["Cats", "Dogs", "Birds", "Lions"])
sectionsArray.append(animals)
return sectionsArray
}
What I tried to do was:
Alamofire.request(.GET, url).validate().responseJSON { response in
switch response.result {
case .Success:
if let value = response.result.value {
let json = JSON(value)
for (_, subJson) in json {
for (year, content) in subJson {
let title = year
let objects = content
sectionsArray.append(Sections(title: title, objects: objects))
}
}
}
case .Failure(let error):
print(error)
}
}
If I print out the results they show in the console - so I know the getting and looping of the JSON works. I then added in
let title = year
let objects = content
sectionsArray.append(Sections(title: title, objects: objects))
But on this line:
sectionsArray.append(Sections(title: title, objects: objects))
I get this error:
cannot convert value of type 'JSON' to expected argument type '[String]'
Here is the JSON I am using:
{"posts": [
{
"Category1": [
"Post1cat1"
],
"Category2": [
"Post1cat2",
"Post2cat2"
]
}
]}
Can someone help me? I might be going in the wrong direction here I want to loop through the JSON and display the categories as headers and the posts in a cell of a table.
edit: 1/29/2016
so, I changed the loop to:
for (_, subJson) in json {
for (index, data) in subJson {
for (title, objects) in data {
sectionsArray.append(Sections(title: title, objects: objects.self.arrayValue.map { $0.string!}))
}
}
}
Still no luck. When I add in some prints (under: sectionsArray.append) to test if there is data:
print("--")
print(title)
print(objects.self.arrayValue.map { $0.string!})
print(Sections(title: title, objects: objects.self.arrayValue.map { $0.string!}))
I get this result in the console:
--
Category1
["Post1cat1"]
Sections(headings: "Category1", items: ["Post1cat1"])
--
Category2
["Post1cat2", "Post2cat2"]
Sections(headings: "Category2", items: ["Post1cat2", "Post2cat2"])
Which shows that the information is there, however when I run the app there are still no results form he JSON just the originally defined section and cells above.
In second parsing method (after edit), you're iterating on array in last loop, so either you could create array there and add each element separately, as in example:
for (title, data) in subJson {
var elements: [String] = []
for (_, object) in data {
if let stringELement = object.rawString() {
elements.append(stringELement)
}
}
sectionsArray.append(Sections(title: title, objects: elements))
}
or if you prefer you can use casted raw array from JSON object as in this example:
for (_, subJson) in json["posts"] {
for (title, data) in subJson {
let optionalCastedObjects = data.arrayObject as? [String]
let unwrappedObjects = optionalCastedObjects ?? []
let section = Sections(title: title, objects: unwrappedObjects)
sectionsArray.append(section)
}
}
That should fix mentioned compilation issue.
But in the end remember that you're using async callback (in your GET request) in synchronous getSectionsFromData method. And you're always will return array before the values from that callback (clojure) will append new data. That will cause, that you're never display the data that you fetched that way.
UPDATE
To do that you should refactor your getSectionsFromData method as below.
func getSectionsFromData(completion: ([Sections]) -> ()) {
var sectionsArray = [Sections]()
Alamofire.request(.GET, url).validate().responseJSON { response in
switch response.result {
case .Success:
if let value = response.result.value {
let json = JSON(value)
for (_, subJson) in json["posts"] {
for (title, data) in subJson {
let optionalCastedObjects = data.arrayObject as? [String]
let unwrappedObjects = optionalCastedObjects ?? []
let section = Sections(title: title, objects: unwrappedObjects)
sectionsArray.append(section)
}
}
completion(sectionsArray)
}
case .Failure(let error):
print(error)
}
}
}
And relevant parts in your UITableViewController class.
var sections: [Sections] = []
override func viewDidLoad() {
super.viewDidLoad()
SectionsData().getSectionsFromData { [weak self](sections: [Sections]) -> () in
self?.sections = sections
self?.tableView.reloadData()
}
}
Your second loop is on the array object hence in that loop year is index value and content is the object at that index.
You need to implement an additional loop to fix the problem i.e:
for (_, subJson) in json {
for (index, data) in subJson {
for (title, objects) in data {
sectionsArray.append(Sections(title: title, objects: objects.arrayValue.map { $0.string!}))
}
}
}
From the SwiftyJSON documentation:
for (key,subJson):(String, JSON) in json {
//Do something you want
}
This indicates that subJson is of type JSON. However, your Sections constructor in the first example is:
Sections(title: "Animals", objects: ["Cats", "Dogs", "Birds", "Lions"])
In your second example, you're calling it as:
Sections(title: title, objects: objects)
Unless you have changed the constructor, it's expecting objects to be an array of strings, not JSON. This is why you're getting an error saying Swift can't convert JSON to String. In your case, the objects JSON is actually an array of strings, so you need to use something like:
Sections(title: title, objects: objects.arrayValue.map { $0.string!})
Related
I have a swift program that reads in stock data in JSON format from multiple files. One such file is laid out with a string date and an associated key value pair. If there was a key for the string date I could create a struct with an init. The code below allows me to read the file into a dictionary but I end up with a string where I want a date. In addition, the only way I know how to sort is via mapping to a key value pair first which produces an array. Any input that would point me in the right direction will be appreciated.
Below is a snippet of the JSON file and the code I use to read it into a dictionary.
Regards,
Chris
{
"20200921": {
"NAV": 173.67997
},
"20200922": {
"NAV": 175.49292
},
"20200923": {
"NAV": 171.35833
},
struct TimeSeriesValue: Decodable {
let value : Double
enum CodingKeys: String, CodingKey {
case value = "NAV"
}
}
class getTimeSeriesData: ObservableObject {
#Published var tmpData : [String:TimeSeriesValue] = [:]
init() {
getData()
}
func getData() {
guard let url = Bundle.main.url(forResource: "JSONDataPrincipal", withExtension: "json")
else {
print("Json file not found")
return
}
let decoder = JSONDecoder()
do {
let data = try Data(contentsOf: url)
self.tmpData = try decoder.decode([String:TimeSeriesValue].self, from: data)
} catch {
print(error)
}
}
}
struct JSONModelPrincipal {
#ObservedObject var prinJSONData = getTimeSeriesData()
}
After some research and testing, I would agree that dictionaries are not designed for being ordered. So, I modified the JSON file as shown below. With this one line modification I have been able to create an init and convert the string to a date. Thank you for the responses.
Chris
{ "Time Series Data" : {
"20200921": {
"NAV": 173.67997
},
"20200922": {
"NAV": 175.49292
},
This question already has an answer here:
Swift 4 JSON Codable - value returned is sometimes an object, others an array
(1 answer)
Closed 3 years ago.
Let's say I have an array of JSON response from the GET method like :
[{
"id":"1",
"Name":"John Doe",
},{
"id":"2",
"Name":"Jane Doe",
}]
And from the POST method using id param I only have 1 object JSON response :
{
"id":"1",
"Name":"John Doe",
}
how can I write a method to decode both the JSON dynamically?
At the moment, this is what I'm using :
func convertJSON<T:Decodable>(result: Any?, model: T.Type) -> T? {
if let res = result {
do {
let data = try JSONSerialization.data(withJSONObject: res, options: JSONSerialization.WritingOptions.prettyPrinted)
return try JSONDecoder().decode(model, from: data)
} catch {
print(error)
return nil
}
} else {
return nil
}
}
The method can be used to decode a single object using dynamic model, but I just can't figure it out to handle a single object / an array of objects dynamically.
The most I can get with is just using a duplicate of the method but replacing T with
[T] in the method parameter and return type, if the response is an array.
I'm open to any suggestion, any help is appreciated, Thank You in advance.
Edit : If this question is duplicate of this , I'm not sure how the marked answer could be a solution.
One solution could be to always return [Model]?.
Inside your function first try to decode as Model, on success return an array with that single decoded object inside it. If this fails then try to decode as [Model], on success return the decoded object else return nil.
Using your sample JSONs I created a struct:
struct Person: Codable {
let id, name: String
enum CodingKeys: String, CodingKey {
case id
case name = "Name"
}
}
Then I created a struct with a couple of methods to decode from either a String or an optional Data.
struct Json2Type<T: Decodable> {
// From data to type T
static public func convertJson(_ data: Data?) -> [T]? {
// Check data is not nil
guard let data = data else { return nil }
let decoder = JSONDecoder()
// First try to decode as a single object
if let singleObject = try? decoder.decode(T.self, from: data) {
// On success return the single object inside an array
return [singleObject]
}
// Try to decode as multiple objects
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return nil }
return multipleObjects
}
// Another function to decode from String
static public func convertJson(_ string: String) -> [T]? {
return convertJson(string.data(using: .utf8))
}
}
Finally call the method you prefer:
Json2Type<Person>.convertJson(JsonAsDataOrString)
Update: #odin_123, a way to have either a Model or [Model] as return value can be accomplish using an enum. We can even add the error condition there to avoid returning optionals. Let's define the enum as:
enum SingleMulipleResult<T> {
case single(T)
case multiple([T])
case error
}
Then the struct changes to something like this:
struct Json2Type<T: Decodable> {
static public func convertJson(_ data: Data?) -> SingleMulipleResult<T> {
guard let data = data else { return .error }
let decoder = JSONDecoder()
if let singleObject = try? decoder.decode(T.self, from: data) {
return .single(singleObject)
}
guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return .error }
return .multiple(multipleObjects)
}
static public func convertJson(_ string: String) -> SingleMulipleResult<T> {
return convertJson(string.data(using: .utf8))
}
}
You can call it the same way we did before:
let response = Json2Type<Person>.convertJson(JsonAsDataOrString)
And use a switch to check every possible response value:
switch(response) {
case .single(let object):
print("One value: \(object)")
case .multiple(let objects):
print("Multiple values: \(objects)")
case .error:
print("Error!!!!")
}
I have a method which performs a GET request to an API:
public func getApiData(completion: #escaping () -> (), fullUrl: String)
{
let session = URLSession.shared
let url = URL(string: fullUrl)!
let task = session.dataTask(with: url) { (data, _, _) -> Void in
if let data = data {
self.serializeToJSON(jsonData: data)
completion()
}
}
task.resume()
}
Using SwiftyJSON I then convert the data into JSON:
private func serializeToJSON(jsonData: Data) {
self.json = JSON(data: jsonData)
print(self.json)
for (index,item) in self.json {
print("hi")
}
}
Printing the full JSON gives:
[{"TenantID":1,"Tenant1":"RAC"},{"TenantID":2,"Tenant1":"VictorMillwell"},{"TenantID":3,"Tenant1":"Comfort"},{"TenantID":4,"Tenant1":"Greenlight"}]
However the JSON can't be iterated through as the print("hi") isn't executed, I'm not sure why, I've looked everywhere on the internet to understand why it doesn't iterate and I cant seem to understand why.
Does anyone know why?
There is a good tutorial here, but in the manual it says you can loop like this:
// If json is .Dictionary
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
// If json is .Array
// The `index` is 0..<json.count's string value
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
if you don't know if it's a dictionary or array, maybe you can do it like this:
switch self.json.type {
case .array:
for (index,subJson):(String, JSON) in self.json {
// Do something you want
}
case .dictionary:
for (key,subJson):(String, JSON) in self.json {
// Do something you want
}
default:
// Do some error handling
}
It isn't clear why you want to enumerate the JSON. It's trivial to decode this JSON in Swift 4:
struct Tenant:Decodable { let TenantID:Int; let Tenant1:String }
let arr = try! JSONDecoder().decode([Tenant].self, from: data)
Now arr is a Swift array of Tenant, where each Tenant has a TenantID property and a Tenant1 property. And now you can do whatever you like with that array, including cycling through it if you wish.
I have a very large JSON file that I have downloaded from the web, and I need to parse this in Swift. The JSON construction is an array of dictionaries. Each dictionary object contains a key of "phone" (referring to the phone number), and whose value is the actual phone number in the form of a string.
What I would like to do, is iterate through the entire list of dictionary objects in the array, and ensure that there are no dictionary objects that have the same value for the key, "phone". If a duplicate is found, I would like to eliminate it from the list, and print it out to the console.
Here is the relevant code that I have:
guard let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
print("error")
return
}
for dict in json! {
//This is where I would do the check
}
How would I accomplish this?
You can do as
var ph = [String]()
var newjson = [[String:String]]()
for dict in json {
if ph.contains(dict["Phone"]!) {
print("duplicate phone \(dict["Phone"]!)")
} else {
ph.append(dict["Phone"]!)
newjson.append(dict)
}
}
print(newjson)
Hare newjson is the new array of dictionary that do not have duplicate phone
Use the array extension method to remove the duplicates from the json object
extension Array where Element: Equatable {
mutating func removeDuplicates() {
var result = [Element]()
for value in self {
if !result.contains(value) {
result.append(value)
}
}
self = result
}
}
Alamofire.request(apiURL, method: .get, parameters:parameters, headers:headers)
.responseJSON { response in
if let result = response.result.value {
let json = JSON(result)
var listArray = json["somekey"].arrayValue
listArray.removeDuplicates()
print(listArray)
}
}
I am trying to parse data which look:
It looks like each record is sequential.. 0, 1, 2 and then within each record there are lots of key value pairs such as the name or showID.
I want to go into each record and only get certain pairs, for example the name, showID and Date.
Here is my code, I am unsure what should be my modal in for item in loop
in other words, how do I get the specific fields into my empty dictionary array?
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if let urlContent = data
{
do
{
var jsonResult:NSDictionary = try NSJSONSerialization.JSONObjectWithData(urlContent, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
if let items = jsonResult["items"] as! NSArray?
{
var emptyArrayOfDictionary = [[String : AnyObject]]()
for item in 0...jsonResult.count
{
}
}
The idea would be to create a struct (or a class) which contains the properties you need, created with an initializer from the values in your dictionaries.
Let's say you want to make "Show" objects containing the show name and the show ID.
You could create a struct like this:
struct Show {
let name:String
let showID:Int
init?(dictionary: [String:AnyObject]) {
guard let name = dictionary["name"] as? String,
let showID = dictionary["showID"] as? Int else {
return nil
}
self.name = name
self.showID = showID
}
}
Then iterate over your dictionaries and pass each one to the struct initializer, something like this:
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if let urlContent = data {
do {
if let jsonResult = try NSJSONSerialization.JSONObjectWithData(urlContent, options: []) as? [String : AnyObject] {
if let items = jsonResult["items"] as? [[String : AnyObject]] {
let shows = items.flatMap { Show(dictionary: $0) }
}
}
} catch {
print(error)
}
}
}
The struct initializer is an Optional one, meaning that if the dictionary does not contain the keys "name" or "showID" it will not create the object and will return nil instead; that's why we're using flatMap to iterate instead of map (because flatMap unwraps the Optionals).
Now you have an array of objects, shows, and you can filter or sort its contents easily with Swift methods like sort, filter, etc.
Each object in the shows array is a Show object and has name and showID properties with the data of your dictionaries.
What flatMap does is create an array of Show objects by iterating (like a loop) over the initial array. On this line:
let shows = items.flatMap { Show(dictionary: $0) }
the $0 represents the current array element. What it means is that for each element in the items array, we take it and create a new Show instance with it, and put the resulting array of objects in the constant shows.
There's also map which is often used, but here the init of our Show struct is an optional init, so it returns an Optional Show, and flatMap knows how to deal with this (it will safely unwrap the optional and ignore the nil ones) where map does not.
If you would like to simplify your son parsing try this Open source https://github.com/SwiftyJSON/SwiftyJSON
With this you access name field of item 0
let userName = json[0]["name"].string