iOS: How to access and use API response data from REST SwiftUi - ios

firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am creating a application that has a callout to my very simple api. I am using the below code but an unable to access the 'if let response' part of the code and unable to get the value of 'Comments' from the returned data from api. Wondering if anyone can help on this.
The data come back fine as
//Print out fine
print(str)
will print my response of {"Comments":"success"}
so just wondering how I can properly use this data and check if success etc
Thanks
func loadData() {
guard let url = URL(string: "myapi.php") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
print("hello1")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
print("hello2")
let str = String(decoding: data, as: UTF8.self)
//Print out fine
print(str)
// Cannot enter this if statement
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
print("hello3")
print(response)
}
return
}
}
}.resume()
}
struct TaskEntry: Codable {
public var Comments: String
}

With the description provided by you, the JSON that returns your API is the following:
{
"Comments": "success"
}
Is that right?
If it is, this part of your code
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data)
Marks that you are decoding data fetched from your app as Array. You specify an array of TaskEntry right here [TaskEntry].self for your data that's not true, you need to decode json with try? JSONDecoder().decode(TaskEntry.self, from: data)
If you hope for an array your api must to return something like this
[
{
"Comments": "success"
}
]
For check if it is right with status code I provide a restructuration of your code explained.
First, protocol Codable it's used to decode and encode data with Encoders and Decoders. In that case, you are working with JSONs so you use JSONDecoder to decode data. You only need decode, so with Decodable protocol It's enough. You don't need to mark Comments with public, because you are working with TaskEntry in your project, so by default (no specified) It's internal. Moreover you don't need var, because, you are not making changes of the property.
Also, properties as standard starts with lowercase, so you can use CodingKeys to keep a quality code standard.
You can check it in Apple Developer site
struct TaskEntry: Decodable {
let comments: String
enum CodingKeys: String, CodingKey {
case comments = "Comments"
}
}
Then, you should to think about your Api errors and defined it. I give you an example:
enum NetworkErrors: Error {
case responseIsNotHTTP
case noDataProvided
case unknown
}
Your load function, should to communicate to external class or struct the TaskEntry or the error. You could use a closure with a Result. Please check the link Hacking with Swift. This has two cases with associated values, failure and success.
Further the response of your UrlSessionTask need to be cast to HTTPURLResponse to get the response http status code. I provide an example of that with your code.
func loadData(result: #escaping (Result<TaskEntry, Error>) -> Void) {
guard let url = URL(string: "myapi.php") else {
result(.failure(URLError(.badURL)))
return
}
let request = URLRequest(url: url) // your method is Get, if not you need to set the http method of the request
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse else {
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.responseIsNotHTTP))
}
return
}
switch response.statusCode {
case 200...299:
if let data = data {
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
result(.success(taskEntry))
} catch {
result(.failure(error))
}
} else {
result(.failure(NetworkErrors.noDataProvided))
}
default:
if let error = error {
print(error.localizedDescription)
result(.failure(error))
} else {
result(.failure(NetworkErrors.unknown))
}
}
}.resume()
}
Additionally, you could pass this code to async/await form with a new function in iOS 15 or MacOS 12 (async/await it's a feature of swift 5.5, but the urlsession function that i've used it's only from ios 15 and macos 12 to up):
#available(macOS 12.0, iOS 15.0, *)
func loadData() async throws -> Result<TaskEntry,Error> {
guard let url = URL(string: "myapi.php") else {
throw URLError(.badURL)
}
do {
let (data, urlResponse) = try await URLSession.shared.data(from: url, delegate: nil)
guard let response = urlResponse as? HTTPURLResponse else {
return .failure(NetworkErrors.responseIsNotHTTP)
}
switch response.statusCode {
case 200...299:
do {
let taskEntry = try JSONDecoder().decode(TaskEntry.self, from: data)
return .success(taskEntry)
} catch {
return .failure(error)
}
default:
return .failure(NetworkErrors.unknown)
}
} catch {
return .failure(error)
}
}
Please, vote up if it's clarify you.
Have a good day :)

Related

The data couldn't be read because it isn't in the correct format

Hello I am creating an app with Xcode and I am having the following problem, I created this API with mockapi.io (if you enter the link you'll see the JSON data) https://62858a2ff0e8f0bb7c057f14.mockapi.io/categorias
If you dont want to enter the link here is how it looks the JSON: (By default the JSON has an array without name as the root and that can't be modified)
[
{
"categorie":"Fruits",
"id":"1"
},
{
"categorie":"Animals",
"id":"2"
},
{
"categorie":"Vegetables",
"id":"3"
},
{
"categorie":"Juices",
"id":"4"
},
{
"categorie":"Alcohol",
"id":"5"
},
{
"categorie":"Desserts",
"id":"6"
}
]
The problem I have is that when I try to decode the data from the API it cant't be readed because is in the wrong format, I am trying to recreate the same code of this youtube video, but with my API: https://www.youtube.com/watch?v=sqo844saoC4
This is how my code looks like:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://62858a2ff0e8f0bb7c057f14.mockapi.io/categorias"
getData(from: url)
}
private func getData(from url: String) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong")
return
}
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
}
catch {
print("failed to convert\(error.localizedDescription)")
}
guard let json = result else {
return
}
print(json.items.categorie) // πŸ‘ˆ HERE ES WHERE THE PRINT HAPPENS
})
task.resume()
}
}
// πŸ‘‡ I THINK THE PROBLEM IS DEFINITELY HERE
struct Response: Codable {
let items: ResultItem
}
struct ResultItem: Codable {
let categorie: String
}
When I execute this the terminal print: "The data couldn't be read becouse it isn't in the correct format."
I am pretty sure the error comes of the way I am calling the data in the structs, so my question is...? How can I exactly call the data from my API's JSON in the code?
yes ,there is an issue in your model you don't need to use the (Response) only use the Model (ResultItem) the JSON isn't complex JSON like that it just array of (ResultItem)
private func getData(from url: String) {
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("something went wrong")
return
}
do {
let result = try JSONDecoder().decode([ResultItem].self, from: data)
print(result)
}
catch {
print("failed to convert\(error.localizedDescription)")
}
})
task.resume()
}
struct ResultItem: Codable {
let categorie: String
}
The response you get is an array of ResultItems rather than a single object, so you need to decode it as an array:
result = try JSONDecoder().decode(Array<ResultItem>.self, from: data)
That said, you won't need the Response struct at all and the type of result will be [ResultItem].

How to define a fallback case if a remote GET request fails?

I recently started with iOS development, and I'm currently working on adding new functionality to an existing app. For this feature I need to obtain a JSON file from a web server. However, if the server is unreachable (no internet/server unavailable/etc), a local JSON needs to be used instead.
In my current implementation I tried using a do catch block, but if there's no internet connection, the app just hangs instead of going to the catch block. JSON parsing and local data reading seem to work fine, the problem is likely in the GET method, as I tried to define a callback to return the JSON data as a separate variable, but I'm not sure if that's the correct way.
What is the best way to handle this scenario?
let url = URL(string: "https://jsontestlocation.com") // test JSON
do {
// make a get request, get the result as a callback
let _: () = getRemoteJson(requestUrl: url!, requestType: "GET") {
remoteJson in
performOnMainThread {
self.delegate.value?.didReceiveJson(.success(self.parseJson(jsonData: remoteJson!)!))
}
}
}
catch {
let localFile = readLocalFile(forName: "local_json_file")
let localJson = parseJson(jsonData: localFile!)
if let localJson = localJson {
self.delegate.value?.didReceiveJson(.success(localJson))
}
}
getRemoteJson() implementation:
private func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
// Method which returns a JSON questionnaire from a remote API
var request = URLRequest(url: requestUrl) // create the request
request.httpMethod = requestType
// make the request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// check if there is any error
if let error = error {
print("GET request error: \(error)")
}
// print the HTTP response
if let response = response as? HTTPURLResponse {
print("GET request status code: \(response.statusCode)")
}
guard let data = data else {return} // return nil if no data
completion(data) // return
}
task.resume() // resumes the task, if suspended
}
parseJson() implementation:
private func parseJson(jsonData: Data) -> JsonType? {
// Method definition
do {
let decodedData = try JSONDecoder().decode(JsonType.self, from: jsonData)
return decodedData
} catch {
print(error)
}
return nil
}
If you don't have to use complex logic with reachability, error handling, request retry etc. just return nil in your completion in case of data task, HTTP and No data errors:
func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
var request = URLRequest(url: requestUrl)
request.httpMethod = requestType
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Task error
guard error == nil else {
print("GET request error: \(error!)")
completion(nil)
return
}
// HTTP error
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("GET request failed: \(response!.description)")
completion(nil)
return
}
// No data
guard let data = data else {
completion(nil)
return
}
completion(data)
}
task.resume()
}
let url = URL(string: "https://jsontestlocation.com")!
getRemoteJson(requestUrl: url, requestType: "GET") { remoteJson in
if let json = remoteJson {
print(json)
...
}
else {
print("Request failed")
...
}
}
func NetworkCheck() -> Bool {
var isReachable = false
let reachability = Reachability()
print(reachability.status)
if reachability.isOnline {
isReachable = true
// True, when on wifi or on cellular network.
}
else
{
// "Sorry! Internet Connection appears to be offline
}
return isReachable
}
Call NetworkCheck() before your API request. If It returns false, read your local json file. if true do remote API call.
Incase after remote API call, any failure check with HTTP header response code.
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
}
I think you need to stop the request from hanging when it’s waiting for a response. The app might be running on a poor connection and be able to get some but not all the data in which case you likely want to failover to the local JSON.
I think you can roughly use what you have but add a timeout configuration on the URLSession as described here: https://stackoverflow.com/a/23428960/312910

How can I pass URL(link to a site) using local JSON parsing into my poject

I am trying to use url(variable 'site') as information type, defining it String but my app crashes causing this thread. How can i resolve this?
Can you show where you fetch and parse the JSON in your code? Also perhaps the Codable struct you are using to handle the JSON.
Something like this:
func parseJSON() {
guard let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json") else { 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([CitizenData].self, from: data) {
// Back to main thread
DispatchQueue.main.async {
// update UI
self.results = decodedResponse
// Save to core data
self.saveToDevice(data: decodedResponse)
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
A snippet of your JSON data might help too.

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

swift JSON login REST with post and get response example

It's my first experience with REST in iOS development with swift. I couldn't find any working or straight (simple) example for doing what i need here.
I have a login backend (https://myaddress.com/rest/login), where I need to pass 2 params: login and password. When I pass good values (user exists in database) I get 2 variables as a result: token (string) and firstLogin (bool). So when I get those values I know that login is successful and I can log in into my app.
So I am begging you for an example (just a simple function) of how to achieve that. If I get working code example I will know how to use it for other rest services in my app. I tried many solutions from tutorials I found, but any of them was working for me.. So to not waste my time searching I would like someone experienced to show me the way to achieve that.
I am not sure if Alamofire is so good to use, I know that swift 4 has it's own build neetwork services and to work with json. Any solution that works would be great.
Also, side question - if I would prefer to use Alamofire, do I need to use swiftyJSON also? Or it's just for parsing?
You can use URLSession if you don't like to import Alamofire in your Project to perform a simple task.
here are some method : GET, POST, DELETE METHODS and tutorial
GET METHOD
func makeGetCall() {
// Set up the URL request
let todoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
// now we have the todo
// let's just print it to prove we can access it
print("The todo is: " + todo.description)
// the todo object is a dictionary
// so we just access the title using the "title" key
// so check for a title and print it if we have one
guard let todoTitle = todo["title"] as? String else {
print("Could not get todo title from JSON")
return
}
print("The title is: " + todoTitle)
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
POST METHOD
func makePostCall() {
let todosEndpoint: String = "https://jsonplaceholder.typicode.com/todos"
guard let todosURL = URL(string: todosEndpoint) else {
print("Error: cannot create URL")
return
}
var todosUrlRequest = URLRequest(url: todosURL)
todosUrlRequest.httpMethod = "POST"
let newTodo: [String: Any] = ["title": "My First todo", "completed": false, "userId": 1]
let jsonTodo: Data
do {
jsonTodo = try JSONSerialization.data(withJSONObject: newTodo, options: [])
todosUrlRequest.httpBody = jsonTodo
} catch {
print("Error: cannot create JSON from todo")
return
}
let session = URLSession.shared
let task = session.dataTask(with: todosUrlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling POST on /todos/1")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
guard let receivedTodo = try JSONSerialization.jsonObject(with: responseData,
options: []) as? [String: Any] else {
print("Could not get JSON from responseData as dictionary")
return
}
print("The todo is: " + receivedTodo.description)
guard let todoID = receivedTodo["id"] as? Int else {
print("Could not get todoID as int from JSON")
return
}
print("The ID is: \(todoID)")
} catch {
print("error parsing response from POST on /todos")
return
}
}
task.resume()
}
DELETE METHOD
func makeDeleteCall() {
let firstTodoEndpoint: String = "https://jsonplaceholder.typicode.com/todos/1"
var firstTodoUrlRequest = URLRequest(url: URL(string: firstTodoEndpoint)!)
firstTodoUrlRequest.httpMethod = "DELETE"
let session = URLSession.shared
let task = session.dataTask(with: firstTodoUrlRequest) {
(data, response, error) in
guard let _ = data else {
print("error calling DELETE on /todos/1")
return
}
print("DELETE ok")
}
task.resume()
}
Thanks #MAhipal Singh for you answer. I'll post here example with Alamafire that I used so it's all in one stack question. It's easier than I though, solutions I tried to use before were not working cause I had problems with pinning certificate about I forgot..
func loginRest(login:String, password:String, deviceId:String){
let urlStr = restServices.REST_MAIN_URL + restServices.REST_LOGIN
let params = ["login":login, "password":password, "deviceId":deviceId]
let paramsJson = try! JSONSerialization.data(withJSONObject: params)
var headers: HTTPHeaders = ["Content-Type": "application/json"]
Alamofire.request(urlStr, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON { (response) in
switch response.result {
case .success:
print("SUKCES with \(response)")
case .failure(let error):
print("ERROR with '\(error)")
}
}
If the post is proper the response is (console print):
SUKCES with SUCCESS: {
firstLogin = 1;
token = "dfkafjkfdsakfadsjfksjkfaadjfkjdfkjfskjfdkafjakfjakfjsafksjdafjy878328hjh";
}

Resources