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

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

Related

How can access one item of struct in swift

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
}

How to save the JSON data to UserDefault in Swift? [duplicate]

This question already has answers here:
saving swifty json array to user defaults
(4 answers)
Closed 2 years ago.
I made a post request using Alamofire. This is my code to fetch data from server using an endpoint:
Alamofire.request("http://192.168.80.21:3204/api/auth/signin", method: .post, parameters: parameters,encoding: JSONEncoding.default, headers: nil).responseJSON {
response in
switch response.result {
case .success(let value):
let json = JSON(value)
print(json)
break
case .failure(let error):
print("Error :- \(error)")
}
}
}
And this is the data i get from the server:
{
"accessToken" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NTIsInJvbGUiOjEsImlhdCI6MTYwMzg2ODUxNCwiZXhwIjoxNjAzOTU0OTE0fQ.y68w8XQfqFZDVgaxiuFuVCOqaI5e5vZ-SfoDB_Ctxro",
"role" : "admin",
"auth" : true
}
I want to save the response to UserDefault for further use. Help me to save the data to Userdefault and retrieve and print data.
Create a model, a struct conforming to Codable
struct Auth: Codable {
let accessToken, role: String
let auth: Bool
}
and extend UserDefaults
extension UserDefaults {
func auth(forKey defaultName: String) -> Auth? {
guard let data = data(forKey: defaultName) else { return nil }
do {
return try JSONDecoder().decode(Auth.self, from: data)
} catch { print(error); return nil }
}
func set(_ value: Auth, forKey defaultName: String) {
let data = try? JSONEncoder().encode(value)
set(data, forKey: defaultName)
}
}
Now you can use auth(forKey:) and set(_:forKey:) to read and write an Auth instance directly.
Drop SwiftyJSON and change the Alamofire part to decode the JSON into the struct with JSONDecoder
Alamofire.request("http://192.168.80.21:3204/api/auth/signin", method: .post, parameters: parameters).responseData {
response in
switch response.result {
case .success(let data):
do {
let auth = try JSONDecoder().decode(Auth.self, from: data)
print(auth)
UserDefaults.standard.set(auth, forKey: "Auth")
} catch { print(error) }
case .failure(let error):
print("Error :- \(error)")
}
}
To read the instance write
let auth = UserDefaults.standard.auth(forKey: "Auth")
accessToken
func setInDefault<T: Codable>(key: String, type: T.Type, data: T?) {
guard let data = data else {
return
}
do {
let encoder = JSONEncoder()
let encodeData = try encoder.encode(data)
let dict:[String:Any] = try JSONSerialization.jsonObject(with: encodeData, options: .allowFragments) as! [String : Any]
let data = NSKeyedArchiver.archivedData(withRootObject: dict)
//set user data in defaults
UserDefaults.standard.set(data, forKey: key)
UserDefaults.standard.synchronize()
} catch {
print(error)
//handle error
}
}
func getFromDefault<T: Codable>(key: String, type: T.Type) -> T? {
let archieved = UserDefaults.standard.value(forKey: key)
let dict = NSKeyedUnarchiver.unarchiveObject(with: archieved as? Data ?? Data())
let decoder = JSONDecoder()
do {
if let dic = dict {
let data = try JSONSerialization.data(withJSONObject: dic, options: .prettyPrinted)
return try decoder.decode(type, from: data)
}
} catch {
print(error)
//handle error
}
return nil
}
You can use the above func. like below.
setInDefault(key: "userData", type: AuthUser.self, data: data)
Here,
Param: data is a generic parameter of type T. So here you need to pass the response data you get from server. (It should be Codable class type)
Param: AuthUser is
struct AuthUser : Codable {
let accessToken : String?
let role: String?
let auth: Bool?
enum CodingKeys: String, CodingKey {
case accessToken = "accessToken"
case role = "role"
case auth = "auth"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
accessToken = try values.decodeIfPresent(String.self, forKey: .accessToken)
role = try values.decodeIfPresent(String.self, forKey: .role)
auth = try values.decodeIfPresent(Bool.self, forKey: .auth)
}
}
To get data:
let data = getFromDefault(key: "userData", type: AuthUser.self)
print(data)
If you use global variable. You can use save method according to your purpose which is written below :
let objects = [yourObjectClass]()
func save() {
let jsonEncoder = JSONEncoder()
if let savedData = try? jsonEncoder.encode(objects) {
let defaults = UserDefaults.standard
defaults.set(savedData, forKey: "objects")
} else {
print("Failed to save objects.")
}
}
}
You can save JSON Model data in Userdefaults Using below function code
func saveData(data: yourModelData){
var userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: yourModelData)
userDefaults.set(encodedData, forKey: "yourKey")
userDefaults.synchronize()
}
and retrive Data from Userdefaults use this function
func getData() -> yourModelData {
var userDefaults = UserDefaults.standard
let decoded = userDefaults.data(forKey: "yourKey")
let decodedData = NSKeyedUnarchiver.unarchiveObject(with: decoded) as! [yourModelData]
return decodedData
}

Decoding json data in swift with two different possible json reponse from server to two different structs

When I make an api request through urlSession to the server I might get a json like
{
"movie": "Avengers",
"director": "Joss Whedon",
}
or like this
{
"apiKey": "invalid"
}
I have two structs to save like this
struct Movie: Codable {
let request: String
let director: String
init(movie:String, director:Sring) {
self.movie = movie
self.director = director
}
}
struct Valid: Codable {
let apiKey: String
init(apiKey:String) {
self.apiKey = apiKey
}
}
Based on the response i want to either decode to first struct or the second struct. How to do that.
if let url = URL(string: "myurl.com") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
guard let json = data as? [String:AnyObject] else {
return
}
do {
if let movie = json["movie"]{
//moview is there. so you can decode to your movie struct
let res = try JSONDecoder().decode(Movie.self, from: data)
}else{
//move is not there.it means error
let res = try JSONDecoder().decode(Valid.self, from: data)
}
} catch let error {
print(error)
}
}
}.resume()
}
Parse json with a single Struct like this
struct RootClass : Codable {
let apiKey : String?
let director : String?
let movie : String?
enum CodingKeys: String, CodingKey {
case apiKey = "apiKey"
case director = "director"
case movie = "movie"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
apiKey = try values.decodeIfPresent(String.self, forKey: .apiKey)
director = try values.decodeIfPresent(String.self, forKey: .director)
movie = try values.decodeIfPresent(String.self, forKey: .movie)
}
}
And check the value of key apiKey if it's nil then use movies and director. or use the value of apiKey
First of all, the Codable types to parse the above 2 JSON responses should look like,
struct Movie: Decodable {
let movie: String
let director: String
}
struct Valid: Decodable {
let apiKey: String
}
There is no need to explicitly create init(). Codable will handle that automatically.
Next, you need to create another Codable type that can handle both the responses, i.e.
enum Response: Decodable {
case movie(Movie)
case valid(Valid)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let data = try container.decode(Movie.self)
self = .movie(data)
} catch {
let data = try container.decode(Valid.self)
self = .valid(data)
}
}
}
Now, you can parse the JSON data like so,
do {
let response = try JSONDecoder().decode(Response.self, from: data)
print(response)
} catch {
print(error)
}

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 {

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