Consuming API endpoints inside of a Dictionary of arrays - Swift 4 - json

I know the question sounds weird but I don't know another way to ask this, first of all, I am playing with the Pokemon API and I am new in swift. what is my problem I am parsing the data to show Pokemon information but the endpoint to show pokemon comes like this:
https://pokeapi.co/api/v2/pokemon/
{
"count": 949,
"previous": null,
"results": [
{
"url": "https://pokeapi.co/api/v2/pokemon/1/",
"name": "bulbasaur"
},
{
"url": "https://pokeapi.co/api/v2/pokemon/2/",
"name": "ivysaur"
},
{
"url": "https://pokeapi.co/api/v2/pokemon/3/",
"name": "venusaur"
},
With a name and other URL to see more about a specific pokemon. I could get the dictionary of array and I can show the pokemon name but I don't know how to get the other data that is in the other endpoint.
This is my code for now:
#IBAction func generatePokemon(_ sender: Any) {
// TODO: Improve API request
let apiUrl = URL(string: "https://pokeapi.co/api/v2/pokemon")
let request = URLRequest(url: apiUrl!)
// Request to Pokemon API
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error == nil {
// Optional binding to get data
if let data = data {
let parsedResult: [String:AnyObject]!
do {
parsedResult = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:AnyObject]
if let resultDictonary = parsedResult["results"] as? [[String:AnyObject]] {
print(resultDictonary[0])
}
} catch {
print("Error in parse json")
}
}
}
}
task.resume()
}
So I am not sure if I need to create another function to get the data of that endpoint and then call that inside my generatePokemon function so I can fill the view with more information? or what is the best way to consume that data.
Here is my repo too if someone wanna see it I have a branch there where I am doing all this first network request.
Github
Thank for your time guys!

Hey I did something like what you need here is my repo:
https://bitbucket.org/pokemonred/pokedexgr2/
Check for the branch pokedexSebas, if you have any questions let me know.
I'm using alamofire to perform the requests.
In the repo you have to take a look into two clases, the first one is: SBBackendManager and SebasObjectMapper.
In the SBBackendManager I have these 2 methods:
func getAllPokemon () { // This will retrieve all pokemon
let url = "https://pokeapi.co/api/v2/pokemon"
Alamofire.request(url).responseObject { (response: DataResponse<SPokemonApiResponse>) in
let pokemonResponse = response.result.value
if let sPokeArray = pokemonResponse?.resultados {
contador = sPokeArray.count
}
}
}
func getPokemon(_ url:String){ // This will retrieve a single pokemon
Alamofire.request(url).responseObject { (response: DataResponse<SPokemon>) in
let spokemon = response.result.value
pokemonArray += [spokemon!]
contador = contador! - 1
}
}
And on SebasObjectMapper I have this:
class SPokemonApiResponse:Mappable{
var resultados:[SPokemonResult]?
required init?(map: Map) { }
func mapping(map: Map) {
resultados <- map["results"]
}
}
class SPokemonResult:Mappable {
var url:String? {
didSet { // HERE: every time a result is added to the array will trigger the get a single pokemon method
let bm = SBackendManager()
bm.getPokemon(url!)
}
}
required init(map:Map) {}
func mapping(map: Map) {
url <- map["url"]
}
}

Related

Fetch data from nested JSON API in SwiftUI (object in array in object in another object)

Beginner here, in a bit over my head with this. ;)
I've found examples that have shown me how to get data from a JSON API feed if the feed is structured as an array of objects, but I don't know how to approach getting the data (specifically, url and title) if the data I'm retrieving comes back in a more complex nested structure like this one:
{
"races": {
"videos": [{
"id": 1,
"url": "firsturl",
"title": "1st Video Title"
}, {
"id": 2,
"url": "secondurl",
"title": "2nd Video Title"
}]
}
}
I've succeeded at get data from another API feed that's structured as a simple array of objects--it's like what's above but without the extra two lead-in objects, namely this: { "races": { "videos":
Here's the code I pieced together from a few examples that worked for the simple array:
import SwiftUI
struct Video: Codable, Identifiable {
public var id: Int
public var url: String
public var title: String
}
class Videos: ObservableObject {
#Published var videos = [Video]()
init() {
let url = URL(string: "https://exampledomain.com/jsonapi")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let videoData = data {
let decodedData = try JSONDecoder().decode([Video].self, from: videoData)
DispatchQueue.main.async {
self.videos = decodedData
}
} else {
print("no data found")
}
} catch {
print("an error occurred")
}
}.resume()
}
}
struct VideosView: View {
#ObservedObject var fetch = Videos()
var body: some View {
VStack {
List(fetch.videos) { video in
VStack(alignment: .leading) {
Text(video.title)
Text("\(video.url)")
}
}
}
}
}
I've spent several hours over a few days reading and watching tutorials, but so far nothing is sinking in to help me tackle the more complex JSON API feed. Any tips would be greatly appreciated!
UPDATE:
With the help of a Swift Playground tutorial and the suggested structs mentioned in the comments below, I've succeeded at retrieving the more complex data, but only in Swift Playgrounds, using this:
import SwiftUI
struct Welcome: Codable {
let races: Races
}
struct Races: Codable {
let videos: [Video]
}
struct Video: Codable {
let id: Int
let url, title: String
}
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
getJSON(urlString: "https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
Now, I'm struggling with how to properly integrate this Playgrounds snippet in to the working SwiftUI file's VideosViews, etc.
UPDATE 2:
import SwiftUI
struct Welcome: Codable {
let races: RaceItem
}
struct RaceItem: Codable {
let videos: [VideoItem]
}
struct VideoItem: Codable {
let id: Int
let url: String
let title: String
}
class Fetcher: ObservableObject {
func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
}
struct JSONRacesView: View {
#ObservedObject var fetch = Fetcher()
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
if let followers = followers {
for result in followers.races.videos {
print(result.title )
}
}
}
var body: some View {
VStack {
List(fetch.tracks) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}
}
There's a great site called QuickType (app.quicktype.io) where you can paste in some JSON and get the Swift structs generated for you. Here's what it gives you:
import Foundation
// MARK: - Welcome
struct Welcome: Codable {
let races: Races
}
// MARK: - Races
struct Races: Codable {
let videos: [Video]
}
// MARK: - Video
struct Video: Codable {
let id: Int
let url, title: String
}
They have a bug in their template generator that mangles the demo line (I've submitted a pull request that is merged but isn't live on the site at the time of this writing), but here's what it should look like:
let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
Using do/try so you can catch the errors, you can decode the data and reach the lower levels by doing:
do {
let welcome = try JSONDecoder().decode(Welcome.self, from: jsonData)
let videos = welcome.races.videos //<-- this ends up being your [Video] array
} catch {
//handle any errors
}
Update, based on your comments and updates:
You chose to go a little bit of a different route than my initial suggestion, but that's fine. The only thing that I would suggest is that you might want to deal with handling errors at some point rather than just returning nil in all of the completions (assuming you need to handle errors -- maybe it just not loading is acceptable).
Here's a light refactor of your code:
class Fetcher: ObservableObject {
#Published var tracks : [VideoItem] = []
private func getJSON<T: Decodable>(urlString: String, completion: #escaping (T?) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
completion(nil)
return
}
guard let data = data else {
completion(nil)
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(T.self, from: data) else {
completion(nil)
return
}
completion(decodedData)
}.resume()
}
func fetchData() {
getJSON(urlString:"https://not-the-real-domain.123/api/") { (followers:Welcome?) in
DispatchQueue.main.async {
self.tracks = followers?.races.videos ?? []
}
}
}
}
struct JSONRacesView: View {
#StateObject var fetch = Fetcher()
var body: some View {
VStack {
List(fetch.tracks, id: \.id) { track in
VStack(alignment: .leading) {
Text(track.title)
Text("\(track.url)")
}
}
}.onAppear {
fetch.fetchData()
}
}
}
You can see that now Fetcher has a #Published property that will store the tracks ([VideoItem]). getJSON is still in fetcher, but now it's private just to show that it isn't meant to be called directly. But, now there's a new function called fetchData() that your view will call. When fetchData gets data back, it sets the #Published property to that data. I used the ?? operator to tell the compiler that if followers is nil, then just use [] instead. This is all in a DispatchQueue.main.async block because the URL call is probably not going to return on the main thread and we need to make sure to always update the UI on the main thread (Xcode will warn you about this at runtime if you update the UI on a different thread).
JSONRacesView calls fetchData in onAppear, which happens exactly when it sounds like it will.
Last thing to note is I used #StateObject instead of #ObservedObject. If you're not on iOS 14 or macOS 11 yet, you could use #ObservedObject instead. There are some differences outside the scope of this answer, but that are easily Google-able.

How do I parse the nested JSON data from Brawl Stars api in Swift?

The API website I used. I made an account -> Documentation -> Brawlers if you want to try it out
Here is part of the JSON I want to parse. I want to print the name inside the "starPowers" array as well as the "gadgets" array.
{
"items":[
{
"id":16000014,
"name":"BO",
"starPowers":[
{
"id":23000090,
"name":"CIRCLING EAGLE"
},
{
"id":23000148,
"name":"SNARE A BEAR"
}
],
"gadgets":[
{
"id":23000263,
"name":"SUPER TOTEM"
},
{
"id":23000289,
"name":"TRIPWIRE"
}
]
}
]
}
I tried this first way of parsing the JSON which worked but I couldn't find a way to print the "name" inside the "starPower" or "gadgets" array.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let apiToken = "abcdefg"
if let url = URL(string: "https://api.brawlstars.com/v1/brawlers") {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("Bearer \(apiToken)", forHTTPHeaderField: "authorization")
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
} else {
guard let data = data else {return}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as AnyObject
print(jsonResult)
if let items = jsonResult["items"] as? NSArray {
for item in items as [AnyObject] {
if let name = item["name"] {
print(name!)
}
if let gadgets = item["gadgets"] {
print(gadgets!)
}
if let starPowers = item["starPowers"]{
print(starPowers!)
}
}
}
} catch {
print("JSON processing failed: \(error.localizedDescription)")
}
}
}.resume()
} else {
print("Something went wrong")
}
}
}
So I added another file with this data:
struct StarPowers: Codable {
let starPowerName: String
}
struct Gadgets: Codable {
let gadgetName: String
}
struct Items: Codable {
let name: String
let starPowers: [StarPowers]
let gadgets: [Gadgets]
}
And replaced the code inside the do statement with this but it returned "JSON processing failed: The data couldn’t be read because it is missing." (Catch statement)
let items = try JSONDecoder().decode(Items.self, from: data)
print(items.name.first ?? "")
I'm still fairly new to Swift in general as well as StackOverflow, so any help or feedback will be greatly appreciated. Thanks!
When using the Codable APIs you want to model all the data you see. So this JSON:
{
"items":[
{
"id":16000014,
"name":"BO",
"starPowers":[
{
"id":23000090,
"name":"CIRCLING EAGLE"
},
{
"id":23000148,
"name":"SNARE A BEAR"
}
],
"gadgets":[
{
"id":23000263,
"name":"SUPER TOTEM"
},
{
"id":23000289,
"name":"TRIPWIRE"
}
]
}
]
}
should result in these structs:
//This represents one star power
struct StarPower: Codable {
let id: Int
let name: String
}
//This represents one gadget
struct Gadget: Codable {
let id: Int
let name: String
}
//This represents one "item" (I think they're brawlers, but I didn't make an account so I can't confirm what the API calls them
struct Item: Codable {
let id: Int
let name: String
let starPowers: [StarPower]
let gadgets: [Gadget]
}
However, the excerpt that you provided is actually of type [String:[Item]] (a dictionary (aka a JSON object) with one string key, with the value being an array of items. You could make a Codable struct to handle that, or you can just do this:
let decoded = try! JSONDecoder().decode([String:[Item]].self, from: data)
let items = decoded["items"]!
//items is an array of type [Item]
// Using your example, it would only have one element.
let element = items.first!
for starPower in element.starPowers {
print(starPower.name)
}
for gadget in element.gadgets {
print(gadget.name)
}
(keep in mind that I'm force-unwrapping and force-trying because I'm assuming that fetching the data worked fine and that it is correct. You should probably check these assumptions and use constructs such as if-let and do-catch)
Name the properties of your codable structs exactly as the keys in the corresponding JSON:
struct StarPower: Codable {
let name: String
}
struct Gadget: Codable {
let name: String
}
struct Item: Codable {
let name: String
let starPowers: [StarPower]
let gadgets: [Gadget]
}
The decoder couldn't find the data, because "gadgetName" and "starPowerName" are not part of the JSON you provided.
Another tip: use singular while naming structs, so one element of gadgets is a gadget. the decoder doesn't care how you name your structs, it only cares about the properties

Data immediately deallocated

I am currently working on a Headline UI Element for my app and I am having an issue with my data being immediately deallocated. I have been trying to get the console to print any data it is receiving, but I am not even seen that in the console. What I am trying to achieve is for the app to connect to its targeted "CDN" and then pull data for the headlines. Using PHP I am preforming a SQL Query to generate an array that the app will then feed off of. When running said scrip the following array is generated.
{"id":"1","title":"Meet ergoWare","header_image":"https://gba.ergoware.io/cache/content/topstory/ergo_news_01.svg","summary":"GBA's New Ergonomic Portal!"}
Stunning array yeah, so the next part is it will then be read and compiled in the app to create headline cards, but the issue is I cannot get the data to to load. I know there is something I am missing, but without the compiler pointing me in the direction I need, I'm fumbling around with it. So here is the Swift code.
import SwiftUI
struct Response: Codable {
var results: [Article]
}
struct Article: Codable, Identifiable {
var id: Int
var title: String
var header_image: String
var summary: String
}
struct HeadlineUI: View {
#State var results = [Article]()
var CDNLink: String
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
ForEach(results) { result in
CardView(image: result.header_image, summary: result.summary, heading: result.title)
}
.onAppear(perform: { self.loadData() })
}
}
}
func loadData() {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.results = decodedResponse.results
print(self.results)
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
struct HeadlineUI_Previews: PreviewProvider {
static var previews: some View {
HeadlineUI(CDNLink: "https://gba.ergoware.io/cdn?funct=fetchNews")
}
}
So what should happen is that it connects to the "CDN" and reads the array. That information then is plugged in and for each index of the results, defined by the id, a card should display with the top story image, article header and a short summary. I am starting to get a little more comfortable with Swift, but little hiccups like this keeps breaking my confidence down haha. Thanks for any teachable moments you can provide.
I found it, follow these steps:
According to your class Response, your service should return this JSON:
{
"results" : [{
"id":1,
"title":"Meet ergoWare",
"header_image":"https://gba.ergoware.io/cache/content/topstory/ergo_news_01.svg",
"summary":"GBA's New Ergonomic Portal!"
}]
}
Give it a try using this url
You should place .onAppear in your HStack not in your ForEach. Here is a the full example:
struct Response: Codable {
var results: [Article]
}
struct Article: Codable, Identifiable {
var id: Int
var title: String
var header_image: String
var summary: String
}
struct HeadlineUI: View {
#State var results = [Article]()
var CDNLink: String
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 20) {
Text("Response:")
ForEach(results) { result in
Text(result.title)
}
}.onAppear(perform: { self.loadData() })
}
}
func loadData() {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
return
} else {
do {
let decodedResponse = try JSONDecoder().decode(Response.self, from: data!)
print(decodedResponse)
self.results = decodedResponse.results
} catch let err {
print("Error parsing: \(err)")
}
}
}.resume()
}
}

How to get data from JSON response in iOS Swift?

Here i have using swiftyJson pod library for response data. normal json response data i could able to get data but for complex i could not make it.
here is my code to get data from response:
private func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let user = "David****"
let password = "**************"
let loginString = "\(user):\(password)"
guard let loginData = loginString.data(using: String.Encoding.utf8) else {
return
}
let base64LoginString = loginData.base64EncodedString()
print("base 64 login :\(base64LoginString)")
let headers = ["Authorization": "Basic \(base64LoginString)"]
// using URL and request getting a json
let request = URLRequest(url: NSURL(string: path)! as URL)
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = headers
let session = URLSession.init(configuration: config)
session.dataTask(with: request) { (data:Data?, response: URLResponse?, error:Error?) in
if let jsonData = data { // if data has a data and success
do {
let json: JSON = try JSON(data: jsonData)
onCompletion(json,nil)
print("json data:\(json)")
}catch {// error
onCompletion(JSON(),error)
}
} else { // if the data is nil
onCompletion(JSON(),error)
}
}.resume()
}
Used this function in viewController.swift
func addDummyData() {
// Call API
RestApiManager.sharedInstance.getRandomUser { (json:JSON) in
// return json from API
if let results = json["results"].array { // get results data from json
print("results:\(results)")
for entry in results { // save data to items.
self.items.append(UserObject(json: entry))
}
print("array= \(self.items)")
DispatchQueue.main.async { // back to the main que and reload table
self.tableView.reloadData()
}
}
}
}
Model class:
import SwiftyJSON
class UserObject {
// var pictureURL: String!
var username: String!
required init(json: JSON) {
// pictureURL = json["picture"]["medium"].stringValue
username = json["WorkOrder"].stringValue
}
}
Here is my json response:
{
"d": {
"results": [
{
"__metadata": {
"id": "http://*******:****/sap/opu/odata/sap/ZPRJ_PM_APPS_IH_SRV/WorkOrderF4Set('000000504780')",
"type": "ZPRJ_PM_APPS_IH_SRV.WorkOrderF4"
},
"WorkOrder": "000000504780",
"Description": "General Maintenance testing"
},
}
}
From json response i'm trying to get WorkOrder and Description
Any help much appreciated pls....
Please read the JSON carefully. The outermost object is a dictionary with a key d.
To get the results array you have to write
if let results = json["d"]["results"].array { ...
And you don't need a class and never declare properties as IUO which are initialized in an init method
struct User {
let workOrder: String
let description: String
init(json: JSON) {
workOrder = json["WorkOrder"].stringValue
description = json["Description"].stringValue
}
}
Side note: Since Swift 4 SwiftyJSON has become obsolete in favor of Codable. It's built-in and much more efficient.

Parsing PHP Web Service Replt In Swift

I am new to Swift but am trying to use a PHP based web service that returns a reply :-
{
responseData = {
1 = {
name = "3D SYSTEMS CORP";
result = success;
};
10 = {
name = "ABERCROMBIE & FITCH CO-CL A";
result = success;
};
};
}
I have assumed this is a valid JSON format but have not been able to work out how to load this data which is just a list of strings into an string array. I have trie code such as this to iterate one rte elements but with no success.
let url = urlComponents.URL
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {
(data, response, error) -> Void in
if let urlContnet = data
{
do {
let jsonResult = try NSJSONSerialization.JSONObjectWithData(urlContnet, options: NSJSONReadingOptions.MutableContainers)
if let nudges = jsonResult["responseData"] as? NSArray {
for nudge in nudges {
print(nudge)
}
}
}
catch
{
print("ERROR")
}
}
}
task.resume()
can anyone help
Thanks
Steve
There are two things here
Your JSON is malformed. You can check the validity of JSON at:
http://jsonlint.com
There are several issues in your JSON. The corrected JSON is here:
{
"responseData": {
"1": {
"name": "3D SYSTEMS CORP",
"result": "success"
},
"10": {
"name": "ABERCROMBIE & FITCH CO-CL A",
"result": "success"
}
}
}
responseData is a dictionary and not an array. Change your code to process this dictionary instead of array.
HTH