Missing Argument for Parameter JSONDecoder URLSession with SwiftUI MVVM - ios

I'm trying to learn how to use an API with SwiftUI using Combine and MVVM. I'm super new to this so this may seem like a dumb question but I've got my WebService and it seems to check out but my ViewModel is asking for stuff that I don't know if it should.
I'd like to be able to search for a game by it's name and it pull from the API.
Here's my Game.swift Model:
struct Game: Codable, Identifiable {
let id: Int
let name: String
}
Simple enough.
Here's my handy WebService: GameService.swift
class GameService {
func getGames(name: String, completion: #escaping (Game?) -> ()) {
guard let url = URL(string: "https://api-v3.igdb.com/games/") else { return }
var request = URLRequest(url: url)
request.setValue("HIDDEN SO YOU CAN'T GET ME IN TROUBLE", forHTTPHeaderField: "user-key")
request.httpBody = "fields id,name".data(using: .utf8, allowLossyConversion: false)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("No data in response: \(error?.localizedDescription ?? "Unknown Error").")
return
}
if let gameResponse = try? JSONDecoder().decode(Game.self, from: data) {
print("Yay")
} else {print("Shit")}
}.resume()
}
}
I'm getting a yellow warning of Value 'gameResponse' was defined but never used... on if let gameResponse = try? JSON decoder blah blah blah.
Ok now the messy boy, I present GameViewModel.swift to you:
class GameViewModel: ObservableObject {
private var gameService: GameService!
#Published var game = Game()
init() {
self.gameService = GameService()
}
var gameName: String = ""
func searchGames() {
if let game = self.gameName.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
getGames(by: name)
}
}
private func getGames(by name: String) {
self.gameService.getGames(name: name) { game in
if let game = game {
DispatchQueue.main.async {
self.game = game
}
}
}
}
}
BIG QUESTION: The #Published var game = Game() is saying that there are is a missing parameter 'from' in call. I haven't implemented from anywhere.
The find searchGames() function is giving me an unresolved identifier error with getGames(by: name). Any ideas?

Your call to Game() in GameViewModel references a constructor that does not exist since your struct does not have any default values.
To correctly initialize game, you must call the initializer with parameters:
Game(id: 1, name: "fred")
The reference to the missing parameter "from" is due to the Codable protocol defining another constructor function:
init(from decoder: Decoder)
and XCode is using this constructor as the default and being helpful by telling you that you need to supply the missing info.
getGames(by: name) requires a parameter called "name" which does not appear to be defined anywhere in the example you give.

Related

How to decode this particular JSON model?

EDIT 2:
After changing the Model struct and calling the JSON decoder without a completion handler, I have managed to get this to work. Thank you all for your help.
Fixed code:
Model
import Foundation
struct RatesResponse: Decodable, Hashable{
let rates: [String: Double]
}
Decoder Class
import Foundation
class RatesModelData{
public var rateCurrency = [String]()
public var rateValue = [Double]()
public func getRates(currency: String){
guard let url = URL(string: "https://api.exchangerate.host/latest?base=\(currency)")
else {
print("URL is invalid")
return
}
var request = URLRequest(url: url)
let dataTask = URLSession.shared.dataTask(with: request){data, response, error in
if let data = data {
do{
let ratesResponse = try JSONDecoder().decode(RatesResponse.self, from: data)
for rate in ratesResponse.rates{
self.rateCurrency.append(rate.key)
self.rateValue.append(rate.value)
}
print(self.rateCurrency)
} catch {
print(error)
}
}
}
dataTask.resume()
}
}
EDIT:
I have changed let rate = Rates to let rates = Rates, modified the decoder class to the following and added a do/catch statement however I am now getting the error "The data given is invalid JSON". I have updated the code snippets as well.
I have this json model:
https://api.exchangerate.host/latest?base=usd
and I cannot for the life of me figure out how to decode it properly. I know its a dictionary and has to be decoded as such but i'm struggling to wrap my head around what i'm doing wrong.
Model:
struct RatesResponse: Decodable, Hashable{
let rates : Rates
}
struct Rates: Decodable, Hashable {
let rates: [String: Double]
}
Decoder Class:
class RatesModelData{
public func getRates(currency: String, _ completionHandler: #escaping(Rates) -> Void){
guard let url = URL(string: "https://api.exchangerate.host/latest?base=\(currency)")
else {
print("URL is invalid")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "ACCEPT")
let dataTask = URLSession.shared.dataTask(with: request){data, response, error in
do{
if let data = data {
if let ratesResponse = try JSONDecoder().decode(RatesResponse?.self, from: data){
completionHandler(ratesResponse.rates)
}
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else{
print("Error with response: \(response)")
return
}
if let error = error {
print("Error Thrown : \(error)")
return
}
} catch {
print(error)
}
}
dataTask.resume()
}
}
I ideally want to get this to be displayed in a list view (SwiftUI), but for now just asking for some advice as to where i've gone wrong with my decoding.
Your object model suggests that the value associated with rates key is another object with another rates key. But that doesn’t match the JSON you have provided. The value associated with top-level rates key is just a dictionary.
So you could do the following and be done with it:
struct ResponseObject: Decodable {
let rates: [String: Double]
}
Or, if you want to capture everything in this JSON, you could add the additional properties:
struct Motd: Decodable {
let msg: String
let url: URL
}
struct ResponseObject: Decodable {
let motd: Motd
let success: Bool
let base: String
let date: Date
let rates: [String: Double]
}
And then:
do {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
let responseObject = try decoder.decode(ResponseObject.self, from: data)
print(responseObject)
} catch {
print(error)
}
FWIW, the base, date, and rates objects likely could/should be optionals, e.g.:
struct ResponseObject: Decodable {
let motd: Motd
let success: Bool
let base: String?
let date: Date?
let rates: [String: Double]?
}
To confirm, we would need to see what a well-formed non-success response looks like. Glancing at the documentation it was not immediately obvious, but something like the above is likely what you want, where JSONDecoder would be able to successfully decode both success responses and failure responses.
I went through the struggles of parsing JSON into structs last year so I feel comfortable helping you out. It's definitely a challenging concept to wrap your head around:
First, let me explain what's wrong with your structs:
struct RatesResponse: Decodable, Hashable{
let rates : Rates
// "rates" is a root-level key in your JSON.
// You're giving it the value of your structure Rates,
// which also has a key titled "rates".
}
struct Rates: Decodable, Hashable {
let rates: [String: Double]
}
Here's what your JSON would look like if these structs could be used to parse it:
{
"rates": {
"rates": {
"AED": 3.671827,
"AFN": 80.098152
}
}
In your actual JSON, your "rates" key has the value of an object (or dictionary, which is nothing more than an array with keys and values) I'm sure you don't want to make a struct with a variable for every single currency, so just use an array in your struct, with an element type of String: Double
struct RatesResponse: Decodable, Hashable{
let rates : [String: Double]
}
JSON key "rates" has a value of a dictionary where the keys are strings and the values are doubles.
After decoding in your URLSession, you can do:
for rate in ratesResponse.rates {
let currency = rate.key // AED, AFN, etc.
let exchangeRate = rate.value // 3.671827, 80.098152, etc.
// With these variables, you can append each of them to a new, separate array for use in your SwiftUI list.
}

Swift struct optional values

I am adding a simple login system to my SwiftUI project. Only I can't quite figure it out.
What the problem is, when a user wants to login and it works. I get this response from the server:
"user": {
"id": 6,
"name": "test",
"email": "test#test.com",
"email_verified_at": null,
"created_at": "2020-07-02T09:37:54.000000Z",
"updated_at": "2020-07-02T09:37:54.000000Z"
},
"assessToken": "test-token"
}
But when something isn't right, the server displays an error message like this:
"message": "The given data was invalid.",
"errors": {
"email": [
"The email field is required."
],
"password": [
"The password field is required."
]
}
}
How can I make sure I parse this information into a structure. At the moment it looks like this.
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? newJSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
// MARK: - Welcome
struct Login: Codable {
let user: User
let assessToken: String
}
// MARK: - User
struct User: Codable {
let id: Int
let name, email: String
let emailVerifiedAt: JSONNull?
let createdAt, updatedAt: String
enum CodingKeys: String, CodingKey {
case id, name, email
case emailVerifiedAt = "email_verified_at"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
This is how i do it now:
class HttpAuth: ObservableObject{
var didChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false{
didSet{
didChange.send(self)
}
}
func checkDetails(email: String, password: String){
guard let url = URL(string: "https://test.ngrok.io/api/login") else {
return
}
let body : [String : String] = ["email" : email, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
let finalData = try! JSONDecoder().decode(Login.self, from: data)
print(finalData)
}.resume()
}
}
Do I have to create a new struct named like LoginError for example, or do I need it inside the existing login struct?
You need to create separate Codable models for both the success and error cases. And then combine them into a single model that you can use for parsing.
Login model:
struct Login: Decodable {
let user: User
let assessToken: String
}
struct User: Decodable {
let id: Int
let name, email: String
let emailVerifiedAt: String?
let createdAt, updatedAt: String
}
Error model:
struct ErrorResponse: Decodable {
let message: String
let errors: Errors
}
struct Errors: Decodable {
let email, password: [String]
}
Combine the Login and ErrorResponse models into Response like so,
enum Response: Decodable {
case success(Login)
case failure(ErrorResponse)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let user = try container.decode(Login.self)
self = .success(user)
} catch {
let error = try container.decode(ErrorResponse)
self = .failure(error)
}
}
}
Now, use Response model to parse your data like so,
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(Response.self, from: data)
switch response {
case .success(let login):
let assessToken = login.assessToken
print(assessToken)
//get any required data from login here..
case .failure(let error):
let message = error.message
}
} catch {
print(error)
}
}.resume()
Or use SwiftyJSON.
If third-party dependency is your concern. Write one yourself.
SwiftyJSON is basically one struct, namely JSON, to refactor out common operations.
The idea behind is simple:
JSON data is dynamic, Codable is largely static.
You don't usually have the luxury of immutable JSON response, hell, API itself changes all the time during development.
You also need to create Codable structs recursively with extra handling of special coding keys.
Codable only makes sense when in small scale or it's auto-gen.
Take another answer for example, you need to define 5 types for a JSON response. Most of them are not reusable.
With JSON, it is var j = JSON(data).
guard let user = j[.user].string else {// error handling} ...
where you replace string user with an enum case case user.
JSON, is reusable, coding key .user is reusable.
Since JSON is nested, you can replace user.id with j[.user][.id].stringValue or j[.user][.id].intValue depending on use case.
And again .user, .id can be added to coding keys enum to be reusable.
You also have great flexibility to adjust to run-time variations.
Ironically, in my opinion, one should use Codable for Encodable, not for Decodable.
Because when you encode, you have full control of its type; when you decode json response, you are at the mercy of backend.
Swift type system is great, but you don't always need the exact type at every step.
JSON is invaluable in deeply nested json response, e.g.; j[.result][.data][0][.user][.hobbies][0][.hobbyName].stringValue. When I'm done, you are still writing first level Codable struct.
Share this here as a comparison so more could appreciate how insanely powerful this is.

How to change value of a Object Struct in SWIFT 5 [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I am trying to get value from JSON type, bring it to my var cryptos. But nothing change, var cryptos still nil. Even value of cryptos in Update void has changed. I have struggled with this problem for many hours. Thanks for your answer.
This is my code:
var cryptos: Crypto? = nil
override func viewDidLoad() {
super.viewDidLoad()
update()
//I can't print this. ERROR: Unexpectedly found nil while unwrapping an Optional value
//print(self.cryptos!.data[0].name)
tableView.delegate = self
tableView.dataSource = self
// Do any additional setup after loading the view.
}
#objc func update() {
if let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest") {
var request = URLRequest(url: url)
request.addValue("305782b4-...-1835adfe147a", forHTTPHeaderField: "X-CMC_PRO_API_KEY")
request.httpMethod = "GET"
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
do {
do {
let response = try JSONDecoder().decode(Crypto.self, from: data!)
self.cryptos = response
//but here I can. Why??
print(self.cryptos!.data[0].name)
} catch { print(error) }
}
}
dataTask.resume()
}
}
This is my Struct:
public struct Crypto : Codable {
struct data : Codable {
let id: Int
let name: String
let symbol: String
let cmc_rank: Int
let slug: String
struct quote : Codable {
struct USD : Codable {
let price: Double
let volume_24h: Double
let percent_change_1h: Double
let percent_change_24h: Double
let percent_change_7d: Double
}
let USD: USD
}
let quote: quote
}
let data: [data]
}
The update runs asynchronously. So supply it a completion handler:
A few suggestions:
I'd define a response object to wrap the payload. The data key is not something you need to perpetuate in your model objects:
public struct ResponseObject<T: Codable>: Codable {
let data: [T]
}
struct Crypto: Codable {
let id: Int
let name: String
let symbol: String
let cmc_rank: Int
let slug: String
struct Quote : Codable {
struct USD : Codable {
let price: Double
let volume_24h: Double
let percent_change_1h: Double
let percent_change_24h: Double
let percent_change_7d: Double
}
let USD: USD
}
let quote: Quote
}
I'd be inclined to use the above generic pattern, so you can reuse this ResponseObject pattern elsewhere.
I'd give update a completion handler that uses the Result type:
#discardableResult
func update(completion: #escaping (Result<[Crypto], Error>) -> Void) -> URLSessionTask {
let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest")!
var request = URLRequest(url: url)
request.addValue("305782b4-...-1835adfe147a", forHTTPHeaderField: "X-CMC_PRO_API_KEY")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let responseData = data, error == nil else {
DispatchQueue.main.async { completion(.failure(error ?? NetworkError.unknownError(data, response))) }
return
}
do {
let response = try JSONDecoder().decode(ResponseObject<Crypto>.self, from: responseData)
DispatchQueue.main.async { completion(.success(response.data)) }
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
}
}
task.resume()
return task
}
Where
enum NetworkError: Error {
case unknownError(Data?, URLResponse?)
}
FYI, in addition to a completion handler closure, I've removed the force unwrapping operator (!).
I'd also make this return the URLSessionTask (in case the caller might, at some future date, want to be able to cancel the request for any reason). By making it #discardableResult, it means that you don't require the caller to do anything with the returned value, but you're leaving that door open for the future.
The viewDidLoad could then use that to reload the table as appropriate:
var cryptos: [Crypto] = []
override func viewDidLoad() {
super.viewDidLoad()
update() { result in
switch result {
case .failure(let error):
print(error)
case .success(let cryptos):
self.cryptos = cryptos
self.tableView.reloadData()
}
}
}
Note, both the update of the model object and the reloading of the table are done in this completion handler, which has been dispatched to the main queue. All UI and model updates should take place on main thread.
Add a completion block to the function because it's an asynchronous task.
override func viewDidLoad() {
super.viewDidLoad()
update {
print(self.cryptos?.data[0].name ?? "")
}
//...
}
#objc func update(completion: #escaping () -> Void) {
if let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest") {
//...
let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
do {
let response = try JSONDecoder().decode(Crypto.self, from: data!)
self.cryptos = response
print(self.cryptos!.data[0].name)
completion()
} catch {
print(error)
completion()
}
}
dataTask.resume()
}
}

Chaining Multiple JSON Request Using Decodable - Swift 5

My "url" object has a link that captures what the user types into a search bar to complete the link then begin the JSON process. The first link responds with another link after the JSON has finished parsing. In my if let validLink = result.link you see I store the link information into an Array. Now I'm not sure if I should begin another JSON response in my if let validLink = result or if I should create a new function like I'm attempting to do in the code below and basically copied and pasted the same JSON information below to reparse it. The second link is getting a parse error. What is the most efficient and right way to do this? I'm really stuck here.
I've tried to create another function that uses the information from the first JSON parse to reparse again using the new link.
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
if let searchText = searchController.searchBar.text, !searchText.isEmpty {
let url = URL(string: "http://djp-dev/api/item?q=\(String(describing: searchText))&dev=1")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
do {
let jsonResult = try JSONDecoder().decode(Response.self, from: data)
let resultsArray = jsonResult.results
for result in resultsArray {
if let validLink = result.link {
print(validLink)
self.collectLink.append(validLink)
self.mainParse()
}
}
} catch {
print("Parse Error")
}
}
task.resume()
}
}
func mainParse() {
let url = URL(string: "http://djp-dev\(collectLink[0])?dev=1")
print(url!)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data,
error == nil else {
//print(error?.localizedDescription ?? "Response Error")
return }
do {
let jsonResult = try JSONDecoder().decode(JSONResponse.self, from: data)
let mainArray = jsonResult.locations
for main in mainArray {
print("""
Manufacture = \(main.rid)
Description = \(main.description)
""")
/*if let validLink = result.description! {
}*/
}
} catch {
print("Parse Error")
}
}
task.resume()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
I basically ask http://djp-dev/api/item?q=\(String(describing: searchText))&dev=1 for a link in the response it sends me back. I want the use the link it sends me to start another JSON request. I'm not sure if I should keep it all into one request or create a whole new function with a whole new JSON request. if let validLink = result.link { } is when I receive the second link information.
So I've figured it out. I was using Decodable without CodingKeys. Thanks to Vadian for pointing me in the right direction. Here's my example:
struct Response : Decodable {
let results: [Results]
enum CodingKeys: String, CodingKey {
case results = "photos"
}
}
struct Results : Decodable {
let url : String?
enum CodingKeys: String, CodingKey {
case url = "url"
}
}
What #vadian is saying is, Codable autmagically uses variable names as a coding key. So you can simply add Decodable like:
struct Response: Decodable {
let results: [Results]
private enum CodingKeys: String, CodingKey {
case results = "photos"
}
}
struct Results: Decodable {
let url: String?
}
and if you change the name of results to photos, you can do
struct Response: Decodable {
let photos: [Results]
}
struct Results: Decodable {
let url: String?
}
On the contrary, if you need a post-processing of data, e.g., converting a String to Date, you need to implement init(from decoder: Decoding) throws yourself. I strongly recommend reading Encoding and decoding custom types.
If you have a sample json, there are many free online tools available to create custom swift structs/classes with codable protocol
one such example is https://app.quicktype.io

Passing JSON result into a struct model

I am receiving a result from an API, I can iterate through the result. My understanding is I can pass the value into a model immediately.
Apple Developer article on struct models
My issue is I am not doing it properly and am receiving a nil value. Perhaps someone can see where I need to change. I am using Swift 4.2
Here is my struct model.
import Foundation
struct ProfileModel {
//MARK: Properties
var name: String
var email: String
var profileURL: String
//MARK: Initialization
}
extension ProfileModel{
init?(json: [String:AnyObject]) {
guard
let name = json["name"] as? String,
let email = json["email"] as? String,
let profileURL = json["profileURL"] as? String
else { return nil }
self.name = name
self.email = email
self.profileURL = profileURL
}
}
Here is my result code from my urlConnection. Let me know if we want to see the entire swift file
//create dataTask using the session object to send data to the server
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:AnyObject] {
self.onSuccess(data: json)
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
func onSuccess(data: [String:AnyObject]){
print("onSuccess")
let myProfile = ProfileModel(json: data)
//myProfile is nil while unwrapping
let title: String = myProfile!.name
print(title)
}
I could just iterate through the strings since I am able to print 'data'. I just figured it would be cleaner to put everything into a ProfileModel and manage that object as a whole.
This json is my more simple one which is why I used it for this question. I also can't remember but I had to use "[String:AnyObject]" to get the json properly. This was pulled directly from my terminal, this was the data being passed in my JsonResponse. The output json from Xcode has [] on the outside instead.
{
'detail': 'VALID',
‘name’: ‘Carson,
'email': ‘carson.skjerdal#somethingelselabs.com',
'pic_url': None
}
EDIT:
So my problem is solved, and ultimately moving to Codable was the key. Here is my fixed code for anyone who might need a working solution.
URLSession.shared.dataTask(with: request as URLRequest) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(ProfileModel.self, from: data)
print(gitData.name)
self.onSuccess(data: gitData)
} catch let err {
print("Err", err)
}
}.resume()
}
func onSuccess(data: ProfileModel){
print("onSuccess")
print(data.email)
}
My Codable Struct - slightly simplified
import Foundation
struct ProfileModel: Codable {
let detail, name, email: String
private enum CodingKeys: String, CodingKey {
case detail, email
case name = "firstname"
//case picUrl = "pic_url"
}
}
After "Codable" has been introduced I always uses that.
You can take your JSON ans pars it in to QuickType.io, and you will get a Struct that confirms to the codadable
// To parse the JSON, add this file to your project and do:
//
// let aPIResponse = try? newJSONDecoder().decode(APIResponse.self, from: jsonData)
import Foundation
struct APIResponse: Codable {
let detail, name, email, picUrl: String
enum CodingKeys: String, CodingKey {
case detail, name, email
case picUrl = "pic_url"
}
}

Resources