SwiftUI JSON Decoder not Working (using async await) - ios

I am creating a new version of an existing app and want to use the new async await
format for a web request. Placing a break at the JSONDecoder().decode line I see that I do have data -
but the decoding does not work. (The url and my key DO work in the old version)
Here's the JSON format of the web source (shortened - there are many more items in
a fuel_station):
{
"station_locator_url":"https://afdc.energy.gov/stations/",
"total_results":110,
"station_counts":{},
"fuel_stations":[
{
"access_code":"public",
"access_days_time":"24 hours daily; call 866-809-4869 for Clean Energy card",
"access_detail_code":"KEY_ALWAYS",
"cards_accepted":"CleanEnergy",
"date_last_confirmed":"2021-09-10",
}
]
}
I created the following models from the above:
enum CodingKeys: String, CodingKey {
case fuelStations = "fuel_stations"
case accessCode = "access_code"
case accessDaysTime = "access_days_time"
case accessDetailCode = "access_detail_code"
case cardsAccepted = "cards_accepted"
case dateLastConfirmed = "date_last_confirmed"
}
struct TopLevel: Codable {
let fuelStations: [FuelStation]
}
struct FuelStation: Codable {
let accessCode, accessDaysTime, accessDetailCode, cardsAccepted: String
let dateLastConfirmed: String
let id: String
}
I put a simplified version of the initial view in one file for testing:
struct SiteListView: View {
#State private var fuelStations: [FuelStation] = []
#State private var topLevel: TopLevel = TopLevel(fuelStations: [])
var body: some View {
NavigationView {
VStack {
List(fuelStations, id: \.id) { item in
VStack {
Text(item.accessCode)
Text(item.accessDaysTime)
}
}
}
.navigationTitle("Site List View")
.task {
await loadData()
}
}//nav
}
func loadData() async {
//I believe the DEMO_KEY in the url will allow limited retrievals
guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY") else {
print("Invalid URL")
return
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { return }
print("response status code is 200")
if let decodedResponse = try? JSONDecoder().decode(TopLevel.self, from: data) {
topLevel = decodedResponse
print("in decoding: topLevel.fuelStations.count is \(topLevel.fuelStations.count)")
//I would iterate through topLevel here and add to the fuelStations
//array but I never get here
}
} catch {
print("Invalid Data")
}
}//load data
}//struct
Any guidance would be appreciated. Xcode 13.2.1 iOS 15.2

First you should remove ? from try? for the catch to work when there is a problem in decoding like this
func loadData() async {
//I believe the DEMO_KEY in the url will allow limited retrievals
guard let url = URL(string: "https://developer.nrel.gov/api/alt-fuel-stations/v1.json?api_key=DEMO_KEY") else {
print("Invalid URL")
return
}
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { return }
print("response status code is 200")
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decodedResponse = try decoder.decode(TopLevel.self, from: data)
print("in decoding: topLevel.fuelStations.count is \(decodedResponse.fuelStations.count)")
//I would iterate through topLevel here and add to the fuelStations
//array but I never get here
} catch {
print(error)
}
}
After you do this , you'll find that some attributes in your struct are coming null in response so you should change string to string? to finally be
struct TopLevel: Codable {
let fuelStations: [FuelStation]
}
struct FuelStation: Codable {
let accessCode, accessDaysTime, accessDetailCode, cardsAccepted,dateLastConfirmed: String?
let id: Int
}
In addition note use of
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
instead of hard-coding the enum

Related

Confused with Codable

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)
}
}
}
}

RxSwift | Best way to make consecutive network requests?

I've been practicing RxSwift recently, but I'm running into a problem in making network requests.
The question is how can I make consecutive network requests .
For example, in Github api, I should use https://api.github.com/user/starred/{\owner}/{\repository_name} to check if the user starred the repository or not.
It should be sent after I received the data requested but I'm having a hard time to implement this.
Here's what I've tried so far:
import RxSwift
// Struct used to encode response
struct RepositoryResponse: Codable {
let items: [Item]
enum CodingKeys: String, CodingKey {
case items
}
struct Item: Codable {
let fullName: String
enum CodingKeys: String, CodingKey {
case fullName = "full_name"
}
}
}
// Actual data for further use
struct Repository {
let item: RepositoryResponse.Item
var fullName: String {
return item.fullName
}
var isStarred: Bool
init(_ item: RepositoryData, isStarred: Bool) {
self.item = item
self.isStarred = isStarred
}
}
// Url components
var baseUrl = URLComponents(string: "https://api.github.com/search/repositories") // base url
let query = URLQueryItem(name: "q", value: "flutter") // Keyword. flutter for this time.
let sort = URLQueryItem(name: "sort", value: "stars") // Sort by stars
let order = URLQueryItem(name: "order", value: "desc") // Desc order
baseUrl?.queryItems = [query, sort, order]
// Observable expected to return Observable<[Repository]>
Observable<URL>.of((baseUrl?.url)!)
.map { URLRequest(url: $0) }
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
return URLSession.shared.rx.response(request: request)
}
.filter { response, data in
return 200..<300 ~= response.statusCode
}
.map { _, data -> [RepositoryResponse.Item] in
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(RepositoryResponse.self, from: data) {
return decoded.items
} else {
print("ERROR: decoding")
return [RepositoryResponse.Item]()
}
}
.map { items -> [Repository] in
let repos = items.map { item -> Repository in
var isStarred: Bool?
/// I want to initialize Repository with isStarred value
/// What should I do in here?
return Repository(item, isStarred: isStarred)
}
return repos
}
What I planned to do is getting repositories by Github search api and then checking if the user has starred each repository.
So I made Repository struct which has two variables containing the name of repository and star status each.
A problem occurs right here. To initialize the Repository struct, I should get star status.
I've tried a completion way, but it seems return before completion returns value.
private func isRepoStarred(name: String, completion: #escaping (Bool) -> Void) {
let isStarredCheckerUrl = URL(string: "https://api.github.com/user/starred/\(name)")!
URLSession.shared.dataTask(with: isStarredCheckerUrl) { _, response, _ in
guard let response = response as? HTTPURLResponse else {
return
}
let code = response.statusCode
if code == 404 {
return completion(false)
} else if code == 204 {
return completion(true)
} else {
return completion(false)
}
}
}
Another way I've tried is making Single observable but don't know how to use this exactly.
func isRepoStarredObs(name: String) -> Single<Bool> {
return Single<Bool>.create { observer in
let isStarredCheckerUrl = URL(string: "https://api.github.com/user/starred/\(name)")!
let task = URLSession.shared.dataTask(with: isStarredCheckerUrl) { _, response, _ in
guard let response = response as? HTTPURLResponse else {
return
}
let code = response.statusCode
if code == 404 {
observer(.success(false))
} else if code == 204 {
observer(.success(true))
} else {
observer(.failure(NSError(domain: "Invalid response", code: code)))
}
}
task.resume()
return Disposables.create { task.cancel() }
}
}
If you have any ideas, please let me know. Thanks.
This gets the starred status:
func isRepoStarred(name: String) -> Observable<Bool> {
URLSession.shared.rx.data(request: URLRequest(url: URL(string: "https://api.github.com/user/starred/\(name)")!))
.map { data in
var result = false
// find out if repo is starred here and return true or false.
return result
}
}
and this is your search.
func searchRepositories() -> Observable<RepositoryResponse> {
var baseUrl = URLComponents(string: "https://api.github.com/search/repositories") // base url
let query = URLQueryItem(name: "q", value: "flutter") // Keyword. flutter for this time.
let sort = URLQueryItem(name: "sort", value: "stars") // Sort by stars
let order = URLQueryItem(name: "order", value: "desc") // Desc order
baseUrl?.queryItems = [query, sort, order]
return URLSession.shared.rx.data(request: URLRequest(url: baseUrl!.url!))
.map { data in
try JSONDecoder().decode(RepositoryResponse.self, from: data)
}
}
That's all you need to make requests.
To combine them you would do this:
let repositories = searchRepositories()
.flatMap {
Observable.zip($0.items.map { item in
isRepoStarred(name: item.fullName).map { Repository(item, isStarred: $0) }
})
}
In general, it's best to reduce the amount of code inside a flatMap as much as possible. Here's a version that breaks the code up a bit better. This version might also be a bit easier to understand what's going on.
let repositories = searchRepositories()
.map { $0.items }
let starreds = repositories
.flatMap { items in
Observable.zip(items.map { isRepoStarred(name: $0.fullName) })
}
let repos = Observable.zip(repositories, starreds) { items, starreds in
zip(items, starreds)
.map { Repository($0, isStarred: $1) }
}

Nothing appearing the console when Decoding JSON in Swift

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)
}

Storing API data into UserDefaults and printing to a list

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.

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()

Resources