I'm trying to fetch movie data using The Movie Database API and display them on a table view, but my code isn't actually getting any data.
class MovieData: NSObject, Decodable {
var movies: [Movie]?
private enum CodingKeys: String, CodingKey {
case movies = "results"
}
}
I think it has something to do with my coding keys because the movie data I want to retrieve are within an array. Seen here. But I'm a little confused with how Codable works.
class Movie: NSObject, Decodable {
var title: String?
var year: String?
var rate: Double?
var posterImage: String?
var overview: String?
private enum MovieKeys: String, CodingKey {
case title
case overview
case releaseDate = "release_date"
case rate = "vote_average"
case posterImage = "poster_path"
}
required init(from decoder: Decoder) throws {
let movieContainer = try decoder.container(keyedBy: MovieKeys.self)
// Get movie info
title = try movieContainer.decode(String.self, forKey: .title)
overview = try movieContainer.decode(String.self, forKey: .overview)
posterImage = try movieContainer.decode(String.self, forKey: .posterImage)
rate = try movieContainer.decode(Double.self, forKey: .rate)
// releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate)
}
}
This is what I'm using to make the api call, so when the user loads into the view the API will fetch movie data and display it on a cell.
override func viewDidLoad() {
super.viewDidLoad()
let requestURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)&language=en-US&page=1")
if let requestURL = requestURL {
Task {
do {
let (data, response) = try await URLSession.shared.data(from: requestURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw MovieListError.invalidServerResponse
}
let decoder = JSONDecoder()
let movie = try decoder.decode(Movie.self, from: data)
print(data)
print(movie)
}
catch {
print(error)
}
}
}
}
It seems your are trying to decode a wrong object. It should be MovieData.self not Movie.self. And then you can iterate over the array of Movie.
override func viewDidLoad() {
super.viewDidLoad()
let requestURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)&language=en-US&page=1")
if let requestURL = requestURL {
Task {
do {
let (data, response) = try await URLSession.shared.data(from: requestURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw MovieListError.invalidServerResponse
}
let decoder = JSONDecoder()
// change Movie.self to MovieData.self
let movies = try decoder.decode(MovieData.self, from: data)
print(data)
print(movies)
}
catch {
print(error)
}
}
}
}
Related
I'm trying to play around with a COVID dataset from Github (link in code below) but when I run the code nothing appears in the console. There are no errors appearing.
Can anyone advise on whats wrong here? Thanks in advance!
struct country: Decodable {
var location: String
var new_cases: Double
var people_fully_vaccinated: Double
}
func getJSON(){
guard let url = URL(string: "https://raw.githubusercontent.com/owid/covid-19-data/68c39808d445fe90b1fe3d57b93ad9be20f796d2/public/data/latest/owid-covid-latest.json") else{
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request){ (data, response, error) in
if let error = error{
print(error.localizedDescription)
return
}
guard let data = data else{
return
}
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode([country].self, from: data) else{
return
}
let countries = decodedData
for country in countries{
print (country.location)
}
}.resume()
}
getJSON()
You need
struct Root: Decodable {
var location: String
var new_cases: Double? // make it optional as it has some objects with nil
var people_fully_vaccinated: Double? // make it optional as it has some objects with nil
}
With
do {
let res = try decoder.decode([String:Root].self, from: data)
let locations = Array(res.values).map { $0.location }
print(locations)
}
catch {
print(error)
}
I was trying to connect my API data to view it in the cell but it seems that I can't get my response and it's always == nil
The code below describes the Country.SWIFT // Model.SWIFT // Response.SWIFT which is showing how can I get my JSON response using Codable
and CountryCell.SWIFT is showing how I used it to call the API
The link to the API image:
Country.SWIFT
struct Country: Decodable {
var CountryName = ""
var CountryImage = ""
var objectId = ""
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case CountryName = "CountryName"
case CountryImage = "CountryImage"
case objectId = "objectId"
}
//MARK: - Json Decoder
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Parsing our attributes
self.CountryName = try container.decode(String.self, forKey: .CountryName)
self.CountryImage = try container.decode(String.self, forKey: .CountryImage)
self.objectId = try container.decode(String.self, forKey: .objectId)
}
}]
Model.SWIFT
protocol ModelDelegate {
func countriesFetched (_ countries: [Country])
}
class Model {
//MARK: - Vars
var delegate: ModelDelegate?
// MARK: - Get Countries
func getCountries () {
// URL Object
let url = URL(string: Constants.API_URL)
guard url != nil else {return}
// URL Session object
let session = URLSession.shared
//Data Task from URLSession object
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error != nil || data == nil {
print(error!.localizedDescription)
return
}
print(data!)
do {
let decoder = JSONDecoder()
let response = try decoder.decode(Response.self, from: data!)
if response.items != nil {
DispatchQueue.main.async {
self.delegate?.countriesFetched(response.items!)
}
}
}
catch {
}
}
// start data task
dataTask.resume()
}
}
Response.SWIFT
struct Response: Decodable {
var items: [Country]? = []
init(from decoder: Decoder) throws {
var itemsContrainer = try decoder.unkeyedContainer()
self.items = try itemsContrainer.decode([Country].self)
}
}
CountryCell.SWIFT
class CountryCell: UICollectionViewCell {
//MARK: - Vars
var country: Country?
//MARK: - Outlets
#IBOutlet weak var imageViewCountryOutlet: UIImageView!
#IBOutlet weak var lblCountryNameOutlet: UILabel!
//MARK: - Creating Cell
func generateCell (_ myCountry: Country) {
self.country = myCountry
guard self.country != nil else { return }
lblCountryNameOutlet.text = country!.CountryName
guard self.country!.CountryImage != "" else {return}
let url = URL(string: self.country!.CountryImage)
guard url != nil else {return}
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { (data, response, error) in
if error == nil || data != nil {
if url!.absoluteString != self.country!.CountryImage {
return
}
let image = UIImage(data: data!)
DispatchQueue.main.async {
self.imageViewCountryOutlet.image = image
}
}
}
dataTask.resume()
}
}
There is no need for the wrapping Response type, decode the list of countries directly:
let decoder = JSONDecoder()
let items = try decoder.decode([Country].self, from: data!)
In your code, when you ask in Response for unkeyedContainer, the expected JSON structure would need additional nested array.
Check decoding errors in the catch block with
do {
...
}
catch {
print(error)
}
I know it sounds bit absurd but in my case, by using class instead of struct and declare all attributes with public var worked for me.
In my app, users scan a barcode and the information about the product is fetched from an API.
I want to create a history section, where users can view the last 10 products.
The result from the API data is stored in a Result type, which for it to be able to be shown in a list, has to be identifiable.
Result is a custom data type that I'm using to store the details of the products from the API call in.
Result
struct Result: Codable, Identifiable {
var id = UUID()
var description: String?
var brand: String?
var ingredients: String?
var image: String?
var upc_code: String?
var return_message: String?
var return_code: String?
enum CodingKeys: String, CodingKey {
case description, brand, ingredients, image, upc_code, return_message, return_code
}
}
This data types store the array of Result which I'll display as a list
History
struct History: Codable {
var results: [Result]
}
Here's the API call:
func loadData(url: String, completion: #escaping (Error?, Result?) -> Void ) {
if let url = URL(string: url) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {return}
do {
let defaults = UserDefaults.standard
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(data) {
var sizeCheck = defaults.object(forKey:"productHistory") as? [Data] ?? [Data]()
if (sizeCheck.count == 10) { //Check if there's more than 10 products already on the history list
sizeCheck.removeLast()
}
sizeCheck.append(encoded) //Add new product to list
defaults.set(sizeCheck, forKey: "productHistory") //Add new list to userDefaults
}
let decoder = JSONDecoder()
let result: Result = try decoder.decode(Result.self, from: data)
completion(nil, result) //Used elsewhere to display the scanned product after it's been added to the history list
}
catch let e {
print(e)
completion(e, nil)
}
}
task.resume()
}
}
This is my view that shows the last 10 products in a list when a button is pressed.
The last 10 products should be stored in UserDefaults with the key productHistory. This is done in the API call LoadData()
struct historyView: View {
#Binding var showingHistory: Bool
#State private var results = [Result]()
var body: some View {
let defaults = UserDefaults.standard
if let products = defaults.object(forKey: "productHistory") as? Data {
if let decodedResponse = try? JSONDecoder().decode(History.self, from: products) {
self.results = decodedResponse.results
}
}
return List(self.results, id: \.id) { item in
Text(item.description!)
}
}
}
To my understanding, the issue is that UserDefaults can't store JSON data. So when the API data is fetched, I store the data as it is, into userdefualts. Then decode it when I need it, like storing it in history or displaying it.
Currently I'm getting a blank list and the if statement below isn't passing.
if let decodedResponse = try? JSONDecoder().decode(History.self, from: products) {
Here's the JSON data from the API if I paste the URL into the browser:
EDIT
Here's my APICall():
func callAPI() -> String {
if (scannedCode.barcode == "") {
return "noneScanned"
}
else {
let hashedValue = scannedCode.barcode.hashedValue("API ID")
//print(hashedValue!)
loadData(url: "URL") { error, result in
if let err = error {
self.APIresult = err.localizedDescription
print(APIresult)
//output error
}
else if (result?.ingredients == nil) {
DispatchQueue.main.async {
self.APIresult = "noIngredients"
}
}
else if (result?.description == nil) {
DispatchQueue.main.async {
self.APIresult = "noDescription"
}
}
else {
DispatchQueue.main.async {
self.APIresult = "success"
}
}
DispatchQueue.main.async {
product.result = result!
//updates view that show's the scanned product, as it's #Published
}
}
return APIresult
}
}
In this section, I want to find what data I have about the product and process it accordingly. Therefore with the solution above, I return a different value depending on if it's got a image or a description etc...
With vadian solution, I've changed it to this:
loadData(url: "URL") { result in
switch result {
case .success(product):
print("success")
case .failure(error):
print("failure")
}
}
As mentioned in the comments you are mixing up Data and Result
First of all drop History and rename Result as Product. We are going to save an array of Product to UserDefaults
struct Product: Codable, Identifiable {
var id = UUID()
var description: String?
var image: String?
var upc_code: String?
var return_message: String?
var return_code: String?
private enum CodingKeys: String, CodingKey {
case description, image, upc_code, return_message, return_code
}
}
In loadData use the generic Result type as closure parameter. After receiving the data decode it to a Product instance, then load the saved array, remove the first(!) item (if necessary) append the new item, save the array back and call completion with the new Product. All potential errors are passed in the failure case.
func loadData(url: String, completion: #escaping (Result<Product,Error>) -> Void ) {
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { completion(.failure(error)); return }
do {
let decoder = JSONDecoder()
let product = try decoder.decode(Product.self, from: data!)
let defaults = UserDefaults.standard
var history = [Product]()
if let readData = defaults.data(forKey:"productHistory") {
do {
history = try decoder.decode([Product].self, from: readData)
if history.count == 10 { history.removeFirst() }
} catch { print(error) }
}
history.append(product)
let saveData = try JSONEncoder().encode(history)
defaults.set(saveData, forKey: "productHistory")
completion(.success(product))
}
catch {
print(error)
completion(.failure(error))
}
}
task.resume()
}
and call it
loadData(url: "URL") { result in
switch result {
case .success(let product):
if product.ingredients == nil {
self.APIresult = "noIngredients"
} else if product.description == nil {
self.APIresult = "noDescription"
} else {
self.APIresult = "success"
}
product.result = product
case .failure(let error):
self.APIresult = error.localizedDescription
print(APIresult)
}
}
In HistoryView (please name structs with starting uppercase letter) get the data from UserDefaults and decode the Product array.
struct HistoryView: View {
#Binding var showingHistory: Bool
#State private var results = [Product]()
var body: some View {
let defaults = UserDefaults.standard
if let historyData = defaults.data(forKey: "productHistory") {
do {
self.results = try JSONDecoder().decode([Product].self, from: historyData)
} catch { print(error) }
}
return List(self.results, id: \.id) { item in
Text(item.description ?? "n/a")
}
}
}
Note: Be aware that the UUID is not being encoded and saved.
And please use more descriptive variable names.
I am trying to read off Connecticut coronavirus data from this API JSON File- "https://data.ct.gov/resource/rf3k-f8fg.json" ( but I'm getting this error from excode that says keyNotFound(CodingKeys(stringValue: "covid19TestsReported", intValue: nil), Swift.DecodingError.Context
However this error, only comes up when I try to access the covid_19_tests_reported property of the file.
Here is my code, can someone please tell me what I'm doing wrong.
import UIKit
class StateViewController: UIViewController {
#IBOutlet weak var testRatioLbl: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
fileprivate func getData(){
let url = URL(string: "https://data.ct.gov/resource/rf3k-f8fg.json")!
URLSession.shared.dataTask(with: url){(data, response, error) in
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let users = try decoder.decode([Users].self, from: data!)
print(users)
}
catch {
print(error)
}
}.resume()
}
func didUpdatePrice(tests: String) {
DispatchQueue.main.async {
self.testRatioLbl.text = tests + " tests"
}
}
func didFailWithError(error: Error) {
print(error)
}
}
class Users: Decodable {
let covid19TestsReported: String
enum CodingKeys: String, CodingKey {
case covid19TestsReported = "covid19TestsReported"
}
required init(from decoder:Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
covid19TestsReported = try values.decode(String.self, forKey: .covid19TestsReported)
}
}
update you class.. your key value is "covid_19_tests_reported" not "covid19TestsReported"
because this field is nil in some cases so make it optional
struct Users: Decodable {
let covid19TestsReported: String?
private enum CodingKeys: String, CodingKey {
case covid19TestsReported = "covid_19_tests_reported"
}
}
Also update your getData
fileprivate func getData(){
let url = URL(string: "https://data.ct.gov/resource/rf3k-f8fg.json")!
URLSession.shared.dataTask(with: url){(data, response, error) in
do {
let decoder = JSONDecoder()
let users = try decoder.decode([Users].self, from: data!)
users.forEach { (user) in
if let cases = user.covid19TestsReported {
print(cases)
}
}
}
catch {
print(error)
}
}.resume()
}
I'm trying to fetch data from an API but I can't get it right and I don't know the issue here:
struct BTCData : Codable {
let close : Double
let high : Double
let low : Double
private enum CodingKeys : Int, CodingKey {
case close = 3
case high = 4
case low = 5
}
}
func fetchBitcoinData(completion: #escaping (BTCData?, Error?) -> Void) {
let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let bitcoin = try JSONDecoder().decode([BTCData].self, from: data).first {
print(bitcoin)
completion(bitcoin, nil)
}
} catch {
print(error)
}
}
task.resume()
}
I'd like to be able to access close in every dict and iterate like that:
var items : BTCData!
for idx in 0..<15 {
let diff = items[idx + 1].close - items[idx].close
upwardMovements.append(max(diff, 0))
downwardMovements.append(max(-diff, 0))
}
I get nil. I don't understand how to decode this kind of API where I need to iterate something which is not inside another dict.
EDIT: The above was solved and I'm now struggling to use [BTCData] in another function.
I am trying to use it here :
func fetchBitcoinData(completion: #escaping ([BTCData]?, Error?) -> Void) {
let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, error ?? FetchError.unknownNetworkError)
return
}
do {
let bitcoin = try JSONDecoder().decode([BTCData].self, from: data); completion(bitcoin, nil)
//let close52 = bitcoin[51].close
//print(bitcoin)
//print(close52)
} catch let parseError {
completion(nil, parseError)
}
}
task.resume()
}
class FindArray {
var items = [BTCData]()
func findArray() {
let close2 = items[1].close
print(close2)
}
}
fetchBitcoinData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let call = FindArray()
call.items = items
call.findArray()
}
EDIT 2: Solved it with [BTCData](). var items : [BTCData] = [] works too
To decode an array of arrays into a struct with Decodable you have to use unkeyedContainer. Since there is no dictionary CodingKeys are useless.
struct BTCData : Decodable {
let timestamp : Int
let open, close, high, low, volume : Double
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
timestamp = try container.decode(Int.self)
open = try container.decode(Double.self)
close = try container.decode(Double.self)
high = try container.decode(Double.self)
low = try container.decode(Double.self)
volume = try container.decode(Double.self)
}
}
You don't have to change your JSONDecoder() line.
...
if let bitcoin = try JSONDecoder().decode([BTCData].self, from: data).first {
print(bitcoin)
completion(bitcoin, nil)
}
Just by adding two lines it's even possible to decode the timestamp into a Date value
struct BTCData : Decodable {
let timestamp : Date
let open, close, high, low, volume : Double
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
timestamp = try container.decode(Date.self)
...
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
if let bitcoin = try decoder.decode([BTCData].self, from: data).first {
print(bitcoin)
completion(bitcoin, nil)
}
To decode the array and get a value at specific index
do {
let bitcoins = try JSONDecoder().decode([BTCData].self, from: data)
let close52 = bitcoins[51].close
print(close52)
...
You need to use JSONSerialization and cast to [[NSNumber]] to get the result needed
UPDATE
Checking this https://docs.bitfinex.com/v2/reference#rest-public-candles I think this is what you are searching for
Try using this
func fetchBitcoinData(completion: #escaping ([BTCData]?, Error?) -> Void) {
let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let array = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[NSNumber]]{
var arrayOfCoinData : [BTCData] = []
for currentArray in array{
arrayOfCoinData.append(BTCData(close: currentArray[2].doubleValue, high: currentArray[3].doubleValue, low: currentArray[4].doubleValue))
}
debugPrint(arrayOfCoinData)
completion(arrayOfCoinData, nil)
}
} catch {
print(error)
completion(nil, error)
}
}
task.resume()
}
Log Result
[BitcoinApiExample.BTCData(close: 7838.8999999999996,...]