How can access one item of struct in swift - ios

i came from javascript and now i'm studying for swift. i want to print one item of my URLSession return but i dont know how i can do this.
My code:
import Foundation
import Dispatch
enum ServiceError: Error {
case invalidURL
case decodeFail(Error)
case network(Error?)
}
struct Address: Codable {
let zipcode: String
let address: String
let city: String
let uf: String
let complement: String?
enum CodingKeys: String, CodingKey {
case zipcode = "cep"
case address = "logradouro"
case city = "localidade"
case uf
case complement = "complemento"
}
}
class Service {
private let baseURL = "https://viacep.com.br/ws"
func get(cep: String, callback: #escaping (Result<Any, ServiceError>) -> Void) {
let path = "/\(cep)/json"
guard let url = URL(string: baseURL + path) else {
callback(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
callback(.failure(.network(error)))
return
}
guard let json: Address = try? JSONDecoder().decode(Address.self, from: data) else {
return
}
callback(.success(json))
}
task.resume()
}
}
do {
let service = Service()
service.get(cep: "1231234") { result in
DispatchQueue.main.async {
switch result {
case let .failure(error):
print(error)
case let .success(data):
print(data)
}
}
}
}
This is my return:
Address(zipcode: "12341231", address: "Teste", city: "Teste", uf: "TE", complement: Optional(""))
I want want my return is like:
print(data.zipcode)
Output: 12341231

Unlike javascript, Swift is strongly typed, so return the Address type
from your func get(cep:...), not Any.
Note, there are many types of errors do deal with when doing a server request.
An error could mean that the response could not be decoded,
due to a bad request, for example, as in the case with cep: "1231234". If you use cep: "01001000", you don't get an error, and all works well.
So I suggest update your code (especially the ServiceError) to cater for the various errors you may get.
Here is the code I used to test my answer:
let service = Service()
// try also "01001000" for no errors
service.get(cep: "1231234") { result in
switch result {
case let .failure(error):
print("\n---> error: \(error)")
case let .success(data):
print("\n---> data: \(data)")
print("---> zipcode: \(data.zipcode)")
}
}
and
class Service {
private let baseURL = "https://viacep.com.br/ws"
// here return Address, not Any
func get(cep: String, callback: #escaping (Result<Address, ServiceError>) -> Void) {
let path = "/\(cep)/json"
guard let url = URL(string: baseURL + path) else {
callback(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
callback(.failure(.network(error)))
return
}
// this shows you what the server is really sending you
print("\n---> data: \(String(data: data, encoding: .utf8)) \n")
guard let httpResponse = response as? HTTPURLResponse else {
callback(.failure(.apiError(reason: "Unknown")))
return
}
switch httpResponse.statusCode {
case 400: return callback(.failure(.apiError(reason: "Bad Request")))
case 401: return callback(.failure(.apiError(reason: "Unauthorized")))
case 403: return callback(.failure(.apiError(reason: "Resource forbidden")))
case 404: return callback(.failure(.apiError(reason: "Resource not found")))
case 405..<500: return callback(.failure(.apiError(reason: "client error")))
case 500..<600: return callback(.failure(.apiError(reason: "server error")))
default:
callback(.failure(.apiError(reason: "Unknown")))
}
// here use a proper do/catch
do {
let address = try JSONDecoder().decode(Address.self, from: data)
callback(.success(address))
} catch {
// could not be decoded as an Address.
callback(.failure(.decodeFail(error)))
}
}
task.resume()
}
}
enum ServiceError: Error {
case invalidURL
case decodeFail(Error)
case network(Error?)
case apiError(reason: String) // <-- here
}

Related

How can I get my iOS app connected with api?

I am a beginner in iOS development. I was trying to use an api URl: https://www.arbeitnow.com/api/job-board-api in my job search iOS app. But nothing shows on my app. I tested the URL in POSTMAN and it returns json(but HTML in description part?). I wrote the code:
func getResults(completed: #escaping (Result<[Results], ErrorMessage>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let _ = error {
completed(.failure(.invalidData))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(.invalidResponse))
return
}
guard let data = data else {
completed(.failure(.invalidData))
return
}
do {
let deconder = JSONDecoder()
deconder.keyDecodingStrategy = .convertFromSnakeCase
let results = try deconder.decode([Results].self, from: data)
completed(.success(results))
} catch {
completed(.failure(.invalidData))
}
}
task.resume()
}
struct Results: Codable {
let slug, companyName, title, resultsDescription: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
enum CodingKeys: String, CodingKey {
case slug
case companyName = "company_name"
case title
case resultsDescription = "description"
case remote, url, tags
case jobTypes = "job_types"
case location
case createdAt = "created_at"
}
}
I used the code in HomeViewController:
override func viewDidLoad() {
super.viewDidLoad()
title = "Home"
collectionView.backgroundColor = UIColor(named: "backgroundMain")
collectionView.register(SearchViewCell.self, forCellWithReuseIdentifier: cellId)
setupSearchBar()
Service.shared.getResults() { [weak self] result in
switch result {
case .success(let results):
print(results)
self?.jobResults = results
DispatchQueue.main.async {
self?.collectionView.reloadData()
}
case .failure(let error):
print(error)
}
}
}
888
I don't know what is wrong with my code. Can anyone help? Thanks!
You are discarding all meaningful error information, which will make this hard to diagnose. If you get an Error object, you should return that:
enum WebServiceError: Error {
case httpError(Data, Int)
}
func getResults(completion: #escaping (Result<[Results], Error>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {
completion(.failure(URLError(.badURL)))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
error == nil
else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
guard 200 ..< 300 ~= response.statusCode else {
completion(.failure(WebServiceError.httpError(data, response.statusCode)))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode([Results].self, from: data)
completion(.success(results.data))
} catch {
completion(.failure(error))
}
}
task.resume()
}
So, that will,
if there was a URLSession error, tell you what the error was;
if there was a non-2xx status code, tell you what the code was (and return the body of the response, too, in case you want to look at that); and
if there was a parsing error, tell you what the parsing error was.
Without something like this, that captures the salient error information, you are flying blind.
In this case, the error is that you are parsing for [Results], but the structure is a dictionary, whose key is data and whose value is a [Results]. You are missing an object for this dictionary that wraps the [Results].
struct ResponseObject: Decodable {
let data: [Posting]
let links: Links
let meta: Meta
}
struct Posting: Decodable {
let slug, companyName, title, description: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
}
struct Links: Decodable {
let first: URL?
let last: URL?
let prev: URL?
let next: URL?
}
struct Meta: Decodable {
let currentPage: Int
let path: URL
let perPage: Int
let from: Int
let to: Int
let terms: String
let info: String
}
func getResults(completion: #escaping (Result<[Posting], Error>) -> Void) {
let urlString = "https://www.arbeitnow.com/api/job-board-api"
guard let url = URL(string: urlString) else {
completion(.failure(URLError(.badURL)))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
error == nil
else {
completion(.failure(error ?? URLError(.badServerResponse)))
return
}
guard 200 ..< 300 ~= response.statusCode else {
completion(.failure(WebServiceError.httpError(data, response.statusCode)))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let results = try decoder.decode(ResponseObject.self, from: data)
completion(.success(results.data))
} catch {
completion(.failure(error))
}
}
task.resume()
}
Your model does not match the JSON you receive from the link you provided:
Using:
struct Root: Codable{
let data: [WorkData]
let links: Links
let meta: Meta
}
// MARK: - Links
struct Links: Codable {
let first: String
let last, prev: String?
let next: String
}
// MARK: - Meta
struct Meta: Codable {
let currentPage, from: Int
let path: String
let perPage, to: Int
let terms, info: String
enum CodingKeys: String, CodingKey {
case currentPage = "current_page"
case from, path
case perPage = "per_page"
case to, terms, info
}
}
struct WorkData: Codable {
let slug, companyName, title, payloadDescription: String
let remote: Bool
let url: String
let tags, jobTypes: [String]
let location: String
let createdAt: Int
enum CodingKeys: String, CodingKey {
case slug
case companyName = "company_name"
case title
case payloadDescription = "description"
case remote, url, tags
case jobTypes = "job_types"
case location
case createdAt = "created_at"
}
}
should solve the problem
Usage:
let root = JsonDecoder().decode(Root.self, from: data)
let firstCompany = root.data[0]
Edit to adress the comment:
This should work!
let results = try decoder.decode([Results].self, from: data)
is your code isnĀ“t it?
instead use:
let root = JsonDecoder().decode(Root.self, from: data)
how could data be missing here?
After that you should either map the root object to your Result type to keep your Viewmodel and completion Handler the way they are now. Or change Viewmodel and completion Handler instead.

Trouble implementing Swift Result<Success, Error> in API request

I am intentionally creating a user that already exists in MongoDB to display an error message to the user, such as "Please choose a different username" or "email already in use" but I am having trouble decoding the server's response.
My model is designed to handle the success(user/token) or error response(message)...
I am able to successfully decode the user object when an account is created on the backend...
What I am doing wrong? I feel like I should be using an enum for the error model somehow??
// POSTMAN RESPONSE WHEN USERNAME ALREADY TAKEN
{
"success": false,
"message": "Please choose a different username"
}
// XCODE ERROR MESSAGE
//...Expected to decode Dictionary<String, Any> but found a string/data
//instead.", underlyingError: nil))
// DATA MODELS
struct UserResponse: Decodable {
let success: Bool
let message: ErrorResponse?
var user: User?
var token: String?
}
struct ErrorResponse: Decodable, Error {
let message: String
}
class LoginService {
static func createAccount(username: String, email: String, password: String,
completion: #escaping(Result <UserResponse, Error>) -> Void) {
let user = UserSignup(username: username, email: email, password: password)
// Create URL code
do {
let encoder = JSONEncoder()
urlRequest.httpBody = try encoder.encode(user)
let dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let jsonData = data else { return }
do {
let responseObject = try JSONDecoder().decode(UserResponse.self, from: jsonData)
switch responseObject.success {
case true:
completion(.success(responseObject))
case false:
// not working
guard let errorMessage = responseObject.message else {
return
}
completion(.failure(errorMessage))
}
} catch {
print(error)
}
}
dataTask.resume()
} catch {
// handle error
}
}
The error says message is a string so declare it as String
struct UserResponse: Decodable {
let success: Bool
let message: String?
var user: User?
var token: String?
}
I feel like I should be using an enum for the error model somehow
That's a good idea to get rid of the optionals. The enum first decodes status and depending on the value it decodes the user and token on success and the error message on failure
struct UserResponse {
let user: User
let token: String
}
enum Response : Decodable {
case success(UserResponse)
case failure(String)
private enum CodingKeys : String, CodingKey { case success, message, user, token }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let success = try container.decode(Bool.self, forKey: .success)
if success {
let user = try container.decode(User.self, forKey: .user)
let token = try container.decode(String.self, forKey: .token)
self = .success(UserResponse(user: user, token: token))
} else {
let message = try container.decode(String.self, forKey: .message)
self = .failure(message)
}
}
}
Then you can replace
let responseObject = try JSONDecoder().decode(UserResponse.self, from: jsonData)
switch responseObject.success {
case true:
completion(.success(responseObject))
case false:
// not working
guard let errorMessage = responseObject.message else {
return
}
completion(.failure(errorMessage))
}
with
let response = try JSONDecoder().decode(Response.self, from: jsonData)
switch response {
case .success(let responseObject):
completion(.success(responseObject))
case .failure(let errorMessage)
completion(.failure(errorMessage))
}

JSON Decoding Not Populating Table View

I am trying to parse data from the website movieDatabase.com However there's some issue decoding the data to json and populating my table view.I am not sure why this is happening. Please I need help spotting out the problem. Here's my code. https://github.com/lexypaul13/Movie-Browser/tree/main/Movie-Browser
struct Movies: Codable {
let overview:String?
let original_title: String?
let poster_path:String
}
struct ApiResponse:Codable, Hashable {
let page:Int
let shows:[Movies]
enum CodingKeys:String, CodingKey {
case page = "page"
case shows = "results"
}
}
class NetworkManger{
enum EndPoint{
case showList
}
static let shared = NetworkManger()
private let baseURL : String
private var apiKeyPathCompononent :String
private init(){
self.baseURL = "https://api.themoviedb.org/3/movie/now_playing?"
self.apiKeyPathCompononent = "api_key=a07e22bc18f5cb106bfe4cc1f83ad8ed"
}
private var jsonDecoder:JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
func get<T:Decodable>(_ endPoints: EndPoint, urlString: String, completed:#escaping(Result<T?,ErroMessage>)->Void){
guard let url = urlBuilder(endPoint: endPoints) else {
completed(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url){ data, response, error in
if let _ = error {
completed(.failure(.unableToComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode==200 else {
print(ErroMessage.invalidResponse.rawValue)
completed(.failure(.invalidResponse))
return
}
guard let data = data else{
completed(.failure(.invalidData))
return
}
do{
let apiResponse = try self.jsonDecoder.decode([T].self, from: data)
DispatchQueue.main.async {
completed(.success(apiResponse as? T))
}
} catch{
print(ErroMessage.invalidData.rawValue)
}
}
task.resume()
}
private func urlBuilder(endPoint:EndPoint )->URL?{
switch endPoint {
case .showList:
return URL(string: baseURL + apiKeyPathCompononent )
}
}
func getMovies(){
NetworkManger.shared.get(.showList, urlString: "") { [weak self] (result: Result<[Movies]?,ErroMessage> ) in
guard let self = self else { return }
switch result{
case .success(let movies):
self.movies = movies ?? []
DispatchQueue.main.async {self.tableView.reloadData()}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
The root object returned from the api is your ApiResult struct. This contains an array of movies (which you have mapped to the shows property of the ApiResult)
You need to change the getMovies function so that the right generic type can be inferred and the json decoder can do the right thing
func getMovies(){
NetworkManger.shared.get(.showList, urlString: "") { [weak self] (result: Result<ApiResult,ErroMessage> ) in
guard let self = self else { return }
switch result{
case .success(let apiResult):
self.movies = apiResult.shows
DispatchQueue.main.async {self.tableView.reloadData()}
case .failure(let error):
print(error.localizedDescription)
}
}
}

Swift error type server response for wrong input from rest API

I hope all you well. I have a question. I have a simple login page with email and password and also I have a user object like that
// MARK: - UserModel
struct UserModel: Codable {
let error: Bool
let desc: String
let user: User
let token: String
}
// MARK: - User
struct User: Codable {
let id: Int
let email, firstName, lastName, lang: String
let status: Int
let referer, star: String?
let phone: String?
let ip: String?
let birth, idNumber: String?
let regionID: String?
let createdAt, updatedAt: String
enum CodingKeys: String, CodingKey {
case id, email
case firstName = "first_name"
case lastName = "last_name"
case lang, status, referer, star, phone, ip, birth
case idNumber = "id_number"
case regionID = "region_id"
case createdAt, updatedAt
}
}
the return type is the upper one(UserModel). If the user entered his/her credentials true there is no problem. But troubles starts if he/she entered the wrong credentials. I can not parse the return value from the server. Always give me error that line.
And the console output is:
Rentover[2343:150674] Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Bool, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "error", intValue: nil)], debugDescription: "Expected to decode Bool but found a dictionary instead.", underlyingError: nil)): file
Here is my login request function. I used codable for simplicity.
class func requestLogIn(router: Router, completion: #escaping (Result<UserModel, Error>) -> ()) {
guard let url = setUrlComponents(router: router).url else { return }
var urlRequest = URLRequest(url: url)
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.httpMethod = router.method
if router.method == "POST"{
let model = LoginModel(email: router.parameters[0], password: router.parameters[1])
urlRequest.httpBody = try? JSONEncoder().encode(model)
}
let dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
guard error == nil else {
print(error?.localizedDescription)
return
}
guard response != nil else {
print("no response")
return
}
guard let data = data else {
print("no data")
return
}
let responseObject = try! JSONDecoder().decode(UserModel.self, from: data)
print(responseObject.user)
DispatchQueue.main.async {
completion(.success(responseObject))
}
}
dataTask.resume()
}
And here is my error struct.
struct LogInError: Codable, Error{
let error: Bool
let desc: String
let fields: [String] ----> 'Edit here old: let fileds: [String'
}
And last my real call function is like that
NetworkService.requestLogIn(router: Router.login(email: nameTextField.text!, passowrd: passwordTextField.text!)) { (result) in
switch result {
case .success(let userModel):
print("RESULT SUCCESS")
print("Hello \(userModel.user.firstName)")
let selectedVC = UIUtils.checkUserStatus(status: userModel.user.status)
self.navigationController?.modalPresentationStyle = .fullScreen
self.navigationController?.pushViewController(selectedVC, animated: true)
case .failure(let error):
print("RESULT FAILED")
print(error)
}
}
I followed that medium link for creating my router and network service. I am very glad and thankful if you help me with that issue. Or give me some advice about networking api's and usage.
[Edit For error response from server]
My request and response message-body frame is also like that:
Have a nice day. And good codding.
To decode two different JSON strings a convenient solution is an enum with associated types because it can represent the success and failure cases very descriptively.
First it decodes the common error key and then it decodes UserModel or LogInError
enum Response : Decodable {
case success(UserModel), failure(LogInError)
private enum CodingKeys : String, CodingKey { case error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let hasError = try container.decode(Bool.self, forKey: .error)
if hasError {
let errorContainer = try decoder.singleValueContainer()
let errorData = try errorContainer.decode(LogInError.self)
self = .failure(errorData)
} else {
let successContainer = try decoder.singleValueContainer()
let successData = try successContainer.decode(UserModel.self)
self = .success(successData)
}
}
}
Use it
class func requestLogIn(router: Router, completion: #escaping (Result<Response, Error>) -> ()) {
...
do {
let responseObject = try JSONDecoder().decode(Response.self, from: data)
print(responseObject)
DispatchQueue.main.async {
completion(.success(responseObject))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
and
NetworkService.requestLogIn(router: Router.login(email: nameTextField.text!, passowrd: passwordTextField.text!)) { (response) in
switch response {
case .success(let result):
switch result {
case .success(let userModel):
print("RESULT SUCCESS")
print("Hello \(userModel.user.firstName)")
let selectedVC = UIUtils.checkUserStatus(status: userModel.user.status)
self.navigationController?.modalPresentationStyle = .fullScreen
self.navigationController?.pushViewController(selectedVC, animated: true)
case .failure(let errorData):
print(errorData)
}
case .failure(let error):
print("RESULT FAILED")
print(error)
}
}
Declare LoginError as a standard Decodable struct
struct LogInError: Decodable {

JSONDecoder Not Parsing Data

I am trying to get data from this URL
https://api.opendota.com/api/heroStats
I have made a struct
struct HeroStats : Decodable {
let localized_name: String
let primary_attr: String
let attack_type: String
let legs: Int
let image: String
}
Top of my View Controller
var heros = [HeroStats]()
func downloadJSON(completed: #escaping () -> ()) {
let url = URL(string: "https://api.opendota.com/api/heroStats")
URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print(error.debugDescription)
}
do {
guard let data = data else { return}
self.heros = try JSONDecoder().decode([HeroStats].self, from: data)
DispatchQueue.main.async {
completed()
}
print(self.heros)
} catch {
print("JSON ERROR")
return
}
}.resume()
}
I always return JSON ERROR for some reason, although everything seems to be correct.
Try to read more on Codable/Encodable in Swift
Encoding and Decoding custom types
You may want to improve your code by making Swift names that differs from JSON names
struct HeroStats: Codable {
let name: String
let primaryAttribute: String
let attackType: String // Better to be an enum also
let legs: Int
let image: String?
enum CodingKeys: String, CodingKey {
case name = "localized_name"
case primaryAttribute = "primary_attr"
case attackType = "attack_type"
case legs
case image = "img"
}
}

Resources