Swift 5, Alamofire, Combine, MVVM throws error - ios

func fetchData(from endpoint:ScrapeAPI )->AnyPublisher<T, APIError>{
return AF.request(endpoint.mockurl,
method: .post,
parameters: endpoint.parameters,
encoder: JSONParameterEncoder.prettyPrinted,
headers: endpoint.headers)
.validate()
.publishDecodable(type:T.self)
.value()
.mapError{_ in APIError.unknown}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
enum APIError:Error {
case decodingError
case errorCOde(Int)
case unknown
}
extension APIError: LocalizedError{
var errorDescription: String? {
switch self {
case .decodingError:
return "Failed to decode the object from the service"
case .errorCOde(let code ):
return "\(code)- Something went wrong"
case .unknown:
return "the error is unknown"
}
}
}
var subscriptions = Set<AnyCancellable>()
fetchData(from:ScrapeAPI.getScrape).sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("Success")
}
},
receiveValue: { value in
print(value)}
).store(in: &subscriptions)
This prints error "unknown". It reaches the API and API responds with a body. It seems it is not getting published to T. Interestingly, Alamofire .responseJSON works. Ideally, I would like to verify T has new JSON data from API. How do I go about it?

Related

Upload media like video and image with Websocket in swift

I am trying to upload and receive image and video media using WebSocket. Image and video are in base64 string format. I am getting an error while receiving the string from the server through socket "Domain=kNWErrorDomainPOSIX Code=40 "Message too long" UserInfo={NSDescription=Message too long}".
//MARK: Receive
func receiveMessage(){
let workItem = DispatchWorkItem{ [weak self] in
self?.webSocket?.receive(completionHandler: { result in
switch result {
case .success(let message):
switch message {
case .data(let data):
//print("data")
case .string(let strMessgae):
//print("strMessgae")
default:
break
}
case .failure(let error):
print("Error Receiving \(error)")
}
}
}
Getting error in case .failure. Please help me out with the error and how to resolve it.
Thanks in advance.
What I see is just a function of WebSocket that receives a Message.
case .data(let data):
In the data case, you will receive data (image, document, video)
case .string(let strMessgae):
//print("strMessgae")
In strMessage, you will be receiving a text message (eg: hi, hello)
public func sendData(data: String) {
webSocket?.send(URLSessionWebSocketTask.Message.data(data.data(using: .utf8)!), completionHandler: { error in
if let error = error {
print(error.localizedDescription)
}
})
}
try sending your image and video in the function above. Websocket has two functions to send message, either by Message or data.
Try this receive function:
public func receiveMessage() {
webSocket?.receive(completionHandler: { [weak self] result in
switch result {
case .success(let message):
DispatchQueue.main.async {
switch message {
case .data(let data):
print("Data is \(data)")
case .string(let text):
print("Receive Message \(text)")
#unknown default:
break
}
}
case .failure(let error):
print(error)
}
self?.receiveMessage()
})
}

how can i cast server error message to AFError

i am unable to find some way to cast custom message to AFError
class APIClient {
#discardableResult
static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return AF.request(route).validate(statusCode: 200..<300).responseDecodable(decoder: decoder) { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
let err = MYError(description: "here goes some custom error message")
let customErr = err .asAFError
completion(.failure(customErr!))
}
}
}
and my custom error message struct
struct MYError : Error{
let description : String
var localizedDescription: String {
return NSLocalizedString(description, comment: "")
}
Do you know what AFError you want to cast your error to? That's the first step to your answer because the AFError is a type of error class not one specific error. Here's my approach.
First I would change the error to an enum Errors defined as an Error just like the Struct you had. Then add your custom error as a case. Define your description and specify there what you would like the error message to be for each case (specific error).
Second, define what your Error should correspond to in an AFError for each case. This way you can specify and pass the data that will be required for some of the AFErrors like the example of SomeOtherError.
enum Errors: Error {
var localizedDescription: String {
var str: String = ""
switch self {
case .MyError: str = "Some custom error message"
case .SomeOtherError: str = "This would be another error and message"
}
return NSLocalizedString(str, comment: "")
}
var asAFError: AFError? {
switch self {
case .MyError: return .explicitlyCancelled
case .SomeOtherError(let data): return .sessionDeinitialized(data)
default: return nil
}
}
case MyError
case SomeOtherError(Data)
}
Lastly use a guard statement incase you throw and error that cannot be defined as an AFError, then pass that error to your completion like normal.
class APIClient {
#discardableResult
static func performRequest<T:Decodable>(route: APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return AF.request(route).validate(statusCode: 200..<300).responseDecodable(decoder: decoder) { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
guard let err = Errors.MyError.asAFError
else { return error }
completion(.failure(err))
}
}
}
}

Alamofire + Combine: Get the HTTP response status code

I am currently using Alamofire which contains Combine support and using it following way:
let request = AF.request(endpoint)
...
request
.publishDecodable(type: T.self, decoder: decoder)
.value()
.eraseToAnyPublisher()
This will publish result and AFError but from subscriber's .sink, I can't find anywhere to get the HTTP status code. What's the best way to get the status code in subscriber?
If you want the response code, don't erase the DataPublisher using .value(). Instead, use the DataResponse you get from the various publish methods, which includes all of the various response information, including status code. You can then .map it into whatever type you need.
For Swift 5.X and Xcode 12.4
For debugging purposes you can intercept the response right before the Combine publisher (publishDecodable()) and get some of the elements of the URL Response, with :
session.request(signedRequest)
.responseJSON { response in
print(response.request) // original URL request
print(response.response) // URL response
print(response.data) // server data
print(response.result) // result of response serialization
}
The easy MVVM way:
func fetchChats() -> AnyPublisher<ChatListModel, AFError> {
let url = URL(string: "Your_URL")!
AF.request(url, method: .get)
.validate()
.publishDecodable(type: ChatListModel.self)
.value()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Later in viewModel
private var subscriptions: Set<AnyCancellable> = []
// some func
dataManager.fetchChats()
.sink {[weak self] completion in
guard let self = self else { return }
switch completion {
case .failure(let error):
switch error.responseCode {
case 401:
//do something with code
default:
print(error.responseCode)
}
print("All errors:\(error)")
case .finished:
break
}
} receiveValue: {[weak self] message in
guard let self = self else { return }
self.message = message
}
.store(in: &subscriptions)

How to generate and handle errors in Swift

I'm working on an app where users need to login. The webapplication uses a token, that users can get from calling a webservice using their username and password. My question now is what's the best way the handle errors that can occur. What I have now:
LoginViewController.swift
self.api.token(forUsername: "a", password: "b") { (result) in
switch (result) {
case .failure(let error):
print("something when wrong \(error)")
case .success(let token):
print("token \(token)")
}
LoginAPIClient (the self.api) using https://github.com/3lvis/Networking
class LoginAPIClient {
enum ResponseError: Error {
case unexpectedDataError
case unknownError
}
enum Result {
case success(String)
case failure(Error)
}
lazy var networking: Networking = {
let networking = Networking(baseURL: APIClient.serverURL())
return networking
}()
func token(forUsername username: String, password: String, completion: #escaping (Result) -> Void) {
let parameters = ["username" : username, "password" : password]
networking.post("/api/login", parameters: parameters) { (result) in
switch result {
case .failure(let response):
return completion(.failure(response.error))
case .success(let response):
if var token = response.headers["Authorization"] as? String {
token = token.replacingOccurrences(of: "Bearer ", with: "")
return completion(.success(token))
}
return completion(.failure(ResponseError.unknownError))
}
}
}
}
Here for example I'm creating my own error return completion(.failure(ResponseError.unknownError)) in case the server responds with a successful status code (200) but somehow the Authorization header is missing from the response.
This works, the only problem is that now when I handle the error in the ViewController I don't know the exact reason why it fails. For example, from the Networking library I get an error code (400 or 401 etc) but this is lost because first it was an NSError. I could use an NSError but somehow this doesn't feel right. Could anyone point me in the right direction?
One of the solutions I thought of was to add an extra enum and then do something like this:
enum Result {
case success(String)
case networkFailure(FailureJSONResponse)
case failure(Error)
}
self.api.token(forUsername: "a", password: "b") { (result) in
switch (result) {
case .failure(let error):
print("something when wrong \(error)")
case .networkFailure(let response):
print("something when wrong \(error)")
case .success(let token):
print("token \(token)")
}
But I rather have just 1 success and 1 failure in the switch.
Every application usually has its own "Error", so in your case you can define
public enum AppError: Swift.Error, CustomStringConvertible {
case networkError(code: Int)
case anotherError(message: String?)
case underlying(Swift.Error)
public var description: String {
switch self {
case .networkError(let code):
return "Network error with code \(code)"
default://I'm not gonna cover all cases, but you should :)
return "Error"
}
}
}
then you can use your approach
enum Result {
case success(String)
case failure(AppError)
}
You can add second parameter to completion block:
func token(forUsername username: String, password: String, completion: #escaping (Result, error: ResponseError?) -> Void)

Completion handler for Alamofire network fetch

I am attempting to create a function which will return a list of custom objects, created from parsing JSON. I am using AlamoFire to download the content. I have written this function which, on success, creates an array of locations to be returned. However, the returns are always nil. My code is below:
func fetchLocations() -> [Location]? {
var locations : [Location]?
Alamofire.request(.GET, myURL)
.responseJSON { response in
switch response.result {
case .Success(let data):
locations = createMapLocations(data)
case .Failure(let error):
print("Request failed with error: \(error)")
}
}
return locations
}
I am pretty positive the issue is that the functioning is returning before the network request is complete. I am new to Swift, and unsure how to handle this. Any help would be appreciated!
You can read more about closures/ completion handlers https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html or google.
func fetchLocations(completionHandler: (locations: [Location]?, error: NSError) -> ()) -> () {
var locations : [Location]?
Alamofire.request(.GET, myURL)
.responseJSON { response in
switch response.result {
case .Success(let data):
locations = createMapLocations(data)
completionHandler(locations, error: nil)
case .Failure(let error):
print("Request failed with error: \(error)")
completionHandler(locations: nil, error: error)
}
}
}
Usage
fetchLocations(){
data in
if(data.locations != nil){
//do something witht he data
}else{
//Handle error here
print(data.error)
}
}

Resources