I have an API that loads into my app:
https://wger.de/api/v2/exercise/?language=2
I have no errors on the API call and no errors in the table view where I run the get data function. Its worked previously perfectly until now. I havent changed any code.
I assumed the servers would be down for the call, but they arent as you can see.
I am running a print 'request was successful' which is displayed in the debug when running, so its getting the data, however the print(self.exercises) returns an empty array hence no table data...any ideas? Here is the API call
open class ApiService: NSObject {
open func getData(completionHandler: #escaping (NSDictionary?, NSError?) -> Void) -> Self {
//loads api filtering by english only
let requestUrl = "https://wger.de/api/v2/exercise/?format=json&language=2"
Alamofire.request(requestUrl, method: .get, encoding: URLEncoding.default)
.responseJSON { response in
switch response.result {
case .success( let data):
print("Request was sucessful")
completionHandler(data as? NSDictionary, nil)
case .failure(let error):
print("Request failed with error: \(error)")
completionHandler(nil, error as NSError?)
}
}
return self
}
Table Function
func getApiData() {
let _ = apiService.getData() {
(data, error) in
if let data = data {
if let results = data["results"] as? [[String:Any]] {
for result in results {
if let exercise = Exercise(dictionary: result) {
self.exercises.append(exercise)
}
}
self.exercisesTableView.reloadData()
print(self.exercises)
}
}
}
}
I am also using a serialization model if that could interfere?
final public class Exercise {
var id: Int
var descrip: String
var name: String
var language: [Int]
var muscles: [Int]
var musclesSecondary: [Int]
var equipment: [Int]
public init?(dictionary: [String: Any]) {
guard
let id = dictionary["id"] as? Int,
let descrip = dictionary["description"] as? String,
let name = dictionary["name"] as? String,
let language = dictionary["language"] as? [Int],
let muscles = dictionary["muscles"] as? [Int],
let musclesSecondary = dictionary["muscles_secondary"] as? [Int],
let equipment = dictionary["equipment"] as? [Int]
else { return nil }
self.id = id
self.descrip = descrip
self.name = name
self.language = language
self.muscles = muscles
self.musclesSecondary = musclesSecondary
self.equipment = equipment
}
Sorry,
I havent changed any code
is wrong.
You added the properties musclesSecondary and language and exactly there is the error: language is single Int rather than an array.
var language: Int
...
let language = dictionary["language"] as? Int,
Related
I am new to Swift and trying basic JSON parsing by following tutorials. I want to print a field of a JSON file, but it is not working.
Although the link exists, and I am using the same link I used for a previous tutorial, it returns rather than moved on to accessing the JSON.
I understand there is an "easier" way to do it in Swift4 using Decoder, but I received an error when I did it that way.
Here is the structure I am using:
struct Tester {
var userId: Int
var id: Int
var title: String
var body: String
init(json: [String: Any]){
userId = json["userId"] as? Int ?? -10
id = json["id"] as? Int ?? -400
title = json["title"] as? String ?? ""
body = json["body"] as? String ?? ""
}
}
And here is the code that is trying to access the JSON entries
#IBAction func printIDTitle(_ sender: Any) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
if let response = response {
print(response)
}
guard let data = data else { return }
do {
print("here 0\n")
guard let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] else {
print(error)
return
}
print("here 0.5\n")
print("here 1\n")
let d = Tester(json: json)
print(d.id)
print(d.title)
print("here 2\n")
} catch let error {
print(error)
}
}.resume()
}
The "here 0" is the only print that shows up.
What could be my issue?
The root is an array so change
guard let json = try JSONSerialization.jsonObject(with: data, options:[]) as? [[String: Any]] else {
print(error)
return
}
Or better
let res = try! JSONDecoder().decode([Root].self, from:data)
struct Root: Codable {
let userId, id: Int
let title, body: String
}
I'm trying to access the first result from this query:
https://www.instagram.com/web/search/topsearch/?query=_myUsername
I'm able to get a JSON object like so:
var request = URLRequest(url: URL(string: api)!)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error ?? "" as! Error)")
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
completionHandler(jsonResponse,nil)
} catch let parsingError {
print("Error", parsingError)
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
}
task.resume()
The result is a JSON object that omits the first user in "users". For example, if I parse the JSON object to get the username of the first user in the result like this...
if let users = jsonResponse!["users"] as? [Any] {
if let first = users.first as? [String: Any] {
if let user = first["user"] as? [String: Any] {
self.igUser = user["username"] as! String
... It returns the username of the 'position = 1' user, while I actually want the 'position = 0' user. Am I parsing this wrong?
As you can see there is a key position you should assume that the list isn't sorted. You have to find the nth element of the list.
The minimal Codable implementation would be:
struct TopSearchAPIResponse: Codable {
let users: [User]
//let places, hashtags: [Type] // As these two are empty arrays you don't know
// their type in advance. So you can omit them
// for now. When you know their type you can
// use them by providing actual type.
let hasMore: Bool
let rankToken: String
let clearClientCache: Bool
let status: String
struct User: Codable {
let position: Int
let user: UserInfo
struct UserInfo: Codable {
let pk: String
let username: String
let fullName: String
let isPrivate: Bool
let profilePicURL: URL
let profilePicID: String?
let isVerified: Bool
let hasAnonymousProfilePicture: Bool
let followerCount: Int
let reelAutoArchive: ReelAutoArchive
let byline: String
let mutualFollowersCount: Int
let unseenCount: Int
private enum CodingKeys: String, CodingKey {
/* This enum is necessary as we want profile_pic_url & profile_pic_id
to be decoded as profilePicURL & profilePicID respectively (instead of
profilePicUrl & profilePicId) so that we follow Swift conventions */
case pk
case username
case fullName
case isPrivate
case profilePicURL = "profilePicUrl"
case profilePicID = "profilePicId"
case isVerified
case hasAnonymousProfilePicture
case followerCount
case reelAutoArchive
case byline
case mutualFollowersCount
case unseenCount
}
enum ReelAutoArchive: String, Codable {
case off
case on
case unset
}
}
}
}
You will use it as:
do {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try jsonDecoder.decode(TopSearchAPIResponse.self, from: data)
if let firstUser = response.users.first(where: { $0.position == 0 }) {
print(firstUser.user.username) // prints "myusernameisverygay"
}
} catch {
print(error)
}
Note: Some modifications had been made after the answer was accepted.
I am working through the Apple App Development Guide and this is the code I am working with right now...
struct CategoryInfo: Codable {
var category: String
var description: String
var logo: String
var mobileCategoryName: String
enum Keys: String, CodingKey {
case category
case description = "descr"
case logo
case mobileCategoryName = "mobileCatName"
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: Keys.self)
self.category = try valueContainer.decode(String.self, forKey: Keys.category)
self.description = try valueContainer.decode(String.self, forKey: Keys.description)
self.logo = try valueContainer.decode(String.self, forKey: Keys.logo)
self.mobileCategoryName = try valueContainer.decode(String.self, forKey: Keys.mobileCategoryName)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let categories = Industry_TableViewController()
categories.fetchCategoryInfo { (category) in
if let category = category {
print(category)
}
}
}
func fetchCategoryInfo(completion: #escaping(CategoryInfo?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let category = try? jsonDecoder.decode(CategoryInfo.self, from: data) {
completion(category)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}
it works fine when my returned JSON is in the following format...
{"category":"Excavators","descr":"Compact, Mid-Sized, Large, Wheeled, Tracked...","logo":"excavators","mobileCatName":"Excavators"}
My struct is created and all the variables are populated correctly. But the API doesn't bring back one category at a time it brings back multiple like so...
[{"category":"Aerial Lifts","descr":"Aerial Lifts, Man Lifts, Scissor Lifts...","logo":"aeriallifts","mobileCatName":"Aerial Lifts"},{"category":"Aggregate Equipment","descr":"Crushing, Screening, Conveyors, Feeders and Stackers...","logo":"aggregateequipment","mobileCatName":"Aggregate"},{"category":"Agricultural Equipment","descr":"Tractors, Harvesters, Combines, Tillers...","logo":"agricultural","mobileCatName":"Agricultural"}]
And I am running into a wall trying to figure out how to get this decoded properly. I've gone down so many routes I don't even know what to search for any more. Can anyone help or point me in a direction.
You need to modify your function to parse an array of categories instead of a single one. You just need to pass the Array<CategoryInfo> metatype to the decode function and modify the function signature such that the completion handler also returns an array.
func fetchCategoryInfo(completion: #escaping ([CategoryInfo]?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let categories = try? jsonDecoder.decode([CategoryInfo].self, from: data) {
completion(categories)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}
try? jsonDecoder.decode([CategoryInfo].self, from: data)
I passed a JSON data to this table view controller. How to get the JSON data and show it on a table view cell?
When I print passedData I receive the following output:
["jobs": <__NSArrayM 0x17005d9d0>
({
jobDate = "2017-08-31";
jobEndTime = 1504144800;
jobID = 87;
jobTime = 1504137600;
},
{
jobDate = "2017-08-31";
jobEndTime = 1504173600;
jobID = 89;
jobTime = 1504170000;
}),
"result": success,
"message": Retrieve Sucessfully]
This is the code I'm using:
var passedData: [String: Any]!
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let jobs = passedData["jobs"] as? [[String:Any]] else {return 0}
return jobs.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "jobCell", for: indexPath)
// jobs[indexPath.row] display jobTime
return cell
}
Instead of giving you a reply on the subject I would try to help understanding the domain you are working with.
The JSON response you are retrieving contains an array (jobs) where each element is an object. In fact, the JSON syntax states that:
In JSON, values must be one of the following data types:
a string
a number
an object (JSON object)
an array
a boolean
null
If you are using JSONSerialization class, then you will have a dictionary that will contain an array of dictionaries.
A simple snippet like this will give you that array
if let jsonArray = jsonDict["jobs"] as? [[String: Any]] {
print(jsonArray)
}
Now, in order to access elements of that array you can do like the following:
let jsonArrayDict = jsonArray[0]
print(jsonArrayDict["jobTime"] ?? 0)
Obviously the code here is not production ready since you need to pay attention to possible crashes of your app.
What I really suggest is to work with a model that can be passed to your table view cell. This approach has these benefits:
avoid using optionals
document your code
unit test your code
etc.
Here an example on how to convert your JSON object into a specific model. Run it in a playground and practice.
struct Job {
let jobDate: String
let jobEndTime: Int
let jobID: Int
let jobTime: Int
}
extension Job {
init?(dict: [String: Any]) {
guard let jobDate = dict["jobDate"] as? String,
let jobEndTime = dict["jobEndTime"] as? Int,
let jobID = dict["jobID"] as? Int,
let jobTime = dict["jobTime"] as? Int else {
return nil
}
self.jobDate = jobDate
self.jobEndTime = jobEndTime
self.jobID = jobID
self.jobTime = jobTime
}
}
extension Job: CustomStringConvertible {
var description: String {
return "Job: \(jobDate) \(jobEndTime) \(jobID) \(jobTime)"
}
}
let jsonString = """
{
"jobs": [
{
"jobDate": "2017-08-31",
"jobEndTime": 1504144800,
"jobID": 87,
"jobTime": 1504137600
},
{
"jobDate2": "2017-08-31",
"jobEndTime": 1504144800,
"jobID": 87,
"jobTime": 1504137600
}
],
"result": "success",
"message": "Retrieve Sucessfully"
}
"""
if let jsonData = jsonString.data(using: .utf8), let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []),
let jsonDict = jsonObject as? [String: Any],
let jsonArray = jsonDict["jobs"] as? [[String: Any]] {
let jobs = jsonArray.flatMap { Job(dict: $0) }
print(jobs)
} else {
print("No Results")
}
This should help you understand the steps needed to parse and use the JSON data that you have. You will likely need to create a custom UITableViewCell which is beyond the scope of this answer. There are plenty of resources online which will explain this part of the process.
You will also need to convert your timestamps to dates, there are alot of answers on StackOverflow that can help with this. Like this one
Mapping JSON Data
let passedJsonStr = "{\"jobs\":[{\"jobDate\":\"2017-08-31\",\"jobEndTime\":1504144800,\"jobID\":87,\"jobTime\":1504137600},{\"jobDate\":\"2017-08-31\",\"jobEndTime\":1504173600,\"jobID\":89,\"jobTime\":1504170000}],\"result\":\"success\",\"message\":\"Retrieve Sucessfully\"}"
struct Job {
var jobDate: String
var jobEndTime: Int
var jobID: Int
var jobTime: Int
init(dict: [String:AnyObject]) {
// unwrap these safely, I'm just giving an example
self.jobDate = dict["jobDate"] as! String
self.jobEndTime = dict["jobEndTime"] as! Int
self.jobID = dict["jobID"] as! Int
self.jobTime = dict["jobTime"] as! Int
}
}
var jobs = [Job]()
if let data = passedJsonStr.data(using: String.Encoding.utf8) {
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] {
if let jsonData = jsonObject["jobs"] as? [[String:AnyObject]] {
jobs = jsonData.map { Job(dict: $0) }
}
print(jobs)
}
}
Displaying in UITableView
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "jobCell", for: indexPath)
let job = self.jobs[indexPath.row]
cell.titleLabel.text = job.jobDate
return cell
}
Im having a really hard time wrapping my head around this process, I have made an API call and received back JSON, I then use a model to serialise the JSON so it can be used easily in my View Controller Table, but im having issues with how to call the API in the View Controller to have the result fit into my serialisation model. I hope I explained it correctly?
Here is the code of my API Request:
open class ApiService: NSObject {
open func getData(completionHandler: #escaping (NSDictionary?, NSError?) -> Void) -> Self {
let requestUrl = "https://wger.de/api/v2/exercise/?format=json"
Alamofire.request(requestUrl, method: .get, encoding: URLEncoding.default)
.responseJSON { response in
switch response.result {
case .success( let data):
completionHandler(data as? NSDictionary, nil)
case .failure(let error):
print("Request failed with error: \(error)")
completionHandler(nil, error as NSError?)
}
}
return self
}
}
and here is the code of my serialisation
final public class Exercise: ResponseObjectSerializable {
var id: Int!
var description: String!
var name: String!
var muscles: String!
var equipment: String!
public init?(response: HTTPURLResponse, representation: Any) {
guard
let representation = representation as? [String: Any],
let id = representation["id"] as? Int,
let description = representation["description"] as? String,
let name = representation["name"] as? String,
let muscles = representation["muscles"] as? String,
let equipment = representation["equipment"] as? String
else { return nil }
self.id = id
self.description = description
self.name = name
self.muscles = muscles
self.equipment = equipment
}
}
But I cant work out how to fit this into my view controller function call which is currently this
let apiService = ApiService()
let searchController = UISearchController(searchResultsController: nil)
var arrRes: [String] = []
var filtered: [String] = []
var searchActive: Bool = false
var id: Int?
var description: String?
var name: String?
var muscles: String?
var equipment: String?
override func viewDidLoad() {
super.viewDidLoad()
exercisesTableView.delegate = self
exercisesTableView.dataSource = self
exerciseSearchBar.delegate = self
getApiData()
}
func getApiData() {
let _ = apiService.getData() {
(data, error) in
if let data = data {
if let arr = data["results"] as? [String] {
self.arrRes = arr
self.exercisesTableView.reloadData()
}
} else if let error = error {
print(error)
}
}
}
First of all the HTTP response does not affect the custom class at all so I left it out.
Second of all the values for keys muscles and equipment are arrays rather than strings.
Third of all since the JSON data seems to be immutable declare the properties in the class as constant (let)
With a few slightly changes this is the custom class
final public class Exercise {
let id : Int
let description: String
let name: String
let muscles : [Int]
let equipment : [Int]
public init?(dictionary: [String: Any]) {
guard
let id = dictionary["id"] as? Int,
let description = dictionary["description"] as? String,
let name = dictionary["name"] as? String,
let muscles = dictionary["muscles"] as? [Int],
let equipment = dictionary["equipment"] as? [Int]
else { return nil }
self.id = id
self.description = description
self.name = name
self.muscles = muscles
self.equipment = equipment
}
}
Then you have to declare the data source array
var exercises = [Exercise]()
And in the method getApiData() populate the array
...
if let results = data["results"] as? [[String:Any]] {
for result in results {
if let exercise = Exercise(dictionary: result) {
self.exercises.append(exercise)
}
}
self.exercisesTableView.reloadData() // might be dispatched on the main thread.
}
Note: Any is used in Swift 3, in Swift 2 replace all occurrences of Any with AnyObject