While Loop increments and re-runs before Promisekit can finish running and return the proper value in Swift - swift

I'm trying to use a while loop with Promisekit with Alamofire to chain four GET requests, return a value, and rerun the four requests with a new parameter. This is the current code that I'm using:
var index = 0
var count = classDictionary["class"]!.count-1
while index <= count {
firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}
index += 1
}
Each of the first three functions returns a promised string value which is then passed onto the next function until the fourth and final function calls another function to parse the downloaded HTML using Kanna.
Ideally I would like all four functions to be called and completed after which the index will increment and run the loop again using the new index number. As a note, the index in passed onto the functions as a way to identify which index in an array a value should be compared to.
For clarity, I have included the code for the parseBooksXML functions below:
func parseBooksXML(index: Int) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("[a-zA-Z]{2,4}", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.abrevCode = (xml["departments"]["department"].withAttr("abrev", result).element!.attribute(by: "id")!.text)
}
catch {
print("Error: \(error)")
}
fulfill(self.abrevCode)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML2(index: Int, key: String) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("\\d\\d\\d", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.courseNumber = (xml["courses"]["course"].withAttr("name", result).element?.attribute(by: "id")?.text)!
}
catch {
print("Error: \(error)")
}
fulfill(self.courseNumber)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML3(index: Int, key: String) -> Promise<String> {
return Promise {fulfill, reject in
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
let xml = SWXMLHash.parse(response.data!)
do {
let range = self.classDictionary["class"]![index].rangeOfString("[a-zA-Z]{1,3}?\\d?\\d?\\d?$", options: .RegularExpressionSearch)
let result = self.classDictionary["class"]![index].substringWithRange(range!)
try self.instructorCode = (xml["sections"]["section"].withAttr("instructor", self.classTeacher[index]).element?.attribute(by: "id")?.text)!
}
catch {
print("Error: \(error)")
}
fulfill(self.instructorCode)
case .Failure(let error):
print(error)
}
}
}
}
func parseBooksXML4(key: String) -> Void {
let headers = [
"Referer": "URL"
]
Alamofire.request(.GET, "URL", headers: headers)
.responseData { response in
switch response.result {
case .Success:
self.parseISBN(String(data: response.data!, encoding: NSUTF8StringEncoding)!)
case .Failure(let error):
print(error)
}
}
}
Any help would be appreciated!

You need to use when:
let count = classDictionary["class"]!.count-1
let promises = (0..<count).map { index -> Promise<ReplaceMe> in
return firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}
}
when(fulfilled: promises).then {
//…
}

Since parseBooksXML4 call is async, you should wrap parseBooksXML4() call to return promise and wait for that to finish before increment index.
firstly {
parseBooksXML(index)
}.then { abrevCode in
self.parseBooksXML2(index, key: abrevCode)
}.then { courseNumber in
self.parseBooksXML3(index, key: courseNumber)
}.then { instructorCode in
self.parseBooksXML4(instructorCode)
}.then { _ in
index += 1
}
}

Related

Swift - "Alamofire.upload" -> "AF.upload" issue with the Upload function

I have upgraded the pod Alamofire 5.0 from an older version,
From the Migrating Guide
https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%205.0%20Migration%20Guide.md
"MultipartFormData’s API has changed and the top level upload methods to create and upload MultipartFormData have been updated to match other request APIs, so it’s not longer necessary to deal with the Result of the multipart encoding."
I have the below function written, i am unable to re-write this in the new version, Can anyone help me on this regard?
note:
I think i need to change the "Alamofire.upload" -> "AF.upload", however its not along is enough to fix the problem,
func uploadPhoto(token:String, userID: String, data: Data, dataUrl: URL) -> Observable<AnyObject?> {
return Observable<AnyObject?>.create({ (observer) -> Disposable in
print(userID);
print(data);
do {
let urlRequest = try URLRequest(url: ResourcePath.Upload.path + "/\(userID)" , method: .post)
Alamofire.upload(multipartFormData: { (multipartData) in
multipartData.append(data, withName: "Filedata", fileName: dataUrl.absoluteURL.lastPathComponent, mimeType: "image/jpeg")
//append(dataUrl, withName: "Filedata", fileName: dataUrl.absoluteString, mimeType: "image/png")
}, with: urlRequest, encodingCompletion: { (result) in
switch result {
case .success(let value, let t1, let t2) :
print(value)
value.responseData(completionHandler: { (dataResponse) in
switch (dataResponse.result) {
case .success(let value) :
print(value)
do {
let jsonData = JSON(dictData)
print("Upload photo response: \(jsonData)")
observer.onNext(jsonData as AnyObject?)
observer.onCompleted()
}
catch {
print(error)
observer.onError(error)
return
}
break
case .failure(let error) :
print(error)
observer.onError(error)
break
}
})
break
case .failure(let error) :
print(error)
observer.onError(error)
break
default:
break
}
})
} catch {
print(error)
observer.onError(error)
}
return Disposables.create {
print("disposed")
}
})
}
Alamofire 5 has removed the need for the encodingCompletion closure when using multipart form encoding. Instead, you can use the normal response handling as seen in other use cases. For example:
AF.upload(multipartFormData: { data in
// Build your multipart form.
}).responseDecodable(of: SomeType.self) { response in
// Handle response.
}

getting error message from server during API call

I have an app where I used RxSwift for my networking by extending ObservableType this works well but the issue I am having now is when I make an API request and there is an error, I am unable to show the particular error message sent from the server. Now how can I get the particular error response sent from the server
extension ObservableType {
func convert<T: EVObject>(to observableType: T.Type) -> Observable<T> where E: DataRequest {
return self.flatMap({(request) -> Observable<T> in
let disposable = Disposables.create {
request.cancel()
}
return Observable<T>.create({observer -> Disposable in
request.validate().responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
if !disposable.isDisposed {
observer.onNext(value)
observer.onCompleted()
}
case .failure(let error):
if !disposable.isDisposed {
observer.onError(NetworkingError(httpResponse: response.response,
networkData: response.data, baseError: error))
observer.onCompleted()
}
}
}
return disposable
})
})
}
}
let networkRetryPredicate: RetryPredicate = { error in
if let err = error as? NetworkingError, let response = err.httpResponse {
let code = response.statusCode
if code >= 400 && code < 600 {
return false
}
}
return true
}
// Use this struct to pass the response and data along with
// the error as alamofire does not do this automatically
public struct NetworkingError: Error {
let httpResponse: HTTPURLResponse?
let networkData: Data?
let baseError: Error
}
response from the server could be
{
"status" : "error",
"message" : " INSUFFICIENT_FUNDS"
}
or
{
"status" : "success",
"data" : " gghfgdgchf"
}
my response is handled like this
class MaxResponse<T: NSObject>: MaxResponseBase, EVGenericsKVC {
var data: T?
public func setGenericValue(_ value: AnyObject!, forUndefinedKey key: String) {
switch key {
case "data":
data = value as? T
default:
print("---> setGenericValue '\(value)' forUndefinedKey '\(key)' should be handled.")
}
}
public func getGenericType() -> NSObject {
return T()
}
}
the error is
return ApiClient.session.rx.request(urlRequest: MaxApiRouter.topupWall(userId: getUser()!.id!, data: body))
.convert(to: MaxResponse<Wall>.self)
In the official Alamofire docs it is mentioned that validate(), without any parameters:
Automatically validates status code within 200..<300 range, and that
the Content-Type header of the response matches the Accept header of
the request, if one is provided.
So if you do not include Alamofire's validate() you are saying that no matter the status code, if the request did get through, you will consider it successful, so that's why it shows nothing in the failure block.
However if you prefer to use it, yes, it will give you an ResponseValidationFailureReason error, but you still have access to the response.data. Try printing it, you should see the expected error response from the server:
if let responseData = response.data {
print(String(data: responseData, encoding: .utf8))
}

Swift: Pass type from property to generic function

For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}

how to use a completion handler to await the completion of a firestore request

I'm slowly getting my head around completion handlers.
Kind of working backwards if I have a firestore query if I wanted to use a completion handler i'd have to use completion() when the firestore query finishes.
But it's setting up the function that still confuses me.
So if this is a function definition that takes a closure as a parameter:
func doSomethingAsync(completion: () -> ()) {
}
I don't quite get how to go from the above func definition and implementing it for something real like a firestore query and request.
query.getDocuments(){ (querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)")
}
else
{
if(querySnapshot?.isEmpty)!
{
print("there's no document")
completion()
}
else
{
for document in querySnapshot!.documents
{
completion()
}
}
}
}
thanks.
update
so for my example could i do something like
func getFirestoreData(userID: String, completion #escaping() -> ()){
//firestore code:
query.getDocuments(){ (querySnapshot, err) in
if let err = err
{
print("executed first")
completion()
}
else
.......
print("executed first")
completion()
}
}
To call the function i'm doing:
getFirestoreData(userID: theUserID) {
print("executes second")
}
print("executes third") after function execution.
What i'd like to happen is the programming awaits the completion() before continuing to execute.
But "executes third" happens first, then "executes first", then "executes second".
Thanks
Here is full example (With API Call)
Note that : status variable is just a key to finger out what is response from server
(0: error from server, 1: success, -1: something wrong in my code)
func logout(handlerBack: #escaping (_ error: Error?, _ status:Int?, _ error_message:String?)->())
{
Alamofire.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil)
.responseJSON { respons in
switch respons.result {
case .failure(let theError):
handlerBack(theError, 0, nil)
case .success(let data):
let json_data = JSON(data)
/// if couldn't fetch data
guard let status = json_data["status"].int else {
handlerBack(nil,-1, "can't find status")
return
}
/// if statuse == 0
guard status == 1 else {
handlerBack (nil, 0 , json_data["errorDetails"].string)
return
}
// that's means everything fine :)
handlerBack(nil, 1 , nil)
}
}
}
And here is the way to implement it :
// call func
self.logout { (error:error, status:Int, error_message:String) in
// if status not 1, figure out the error
guard status == 1 else {
// try to find error from server
guard let error_message = error_message else {
// else error something else
print ("Error at :: \(#function)")
// don't do anything ..just return
return
}
self.showMessageToUser(title: error_message, message: "", ch: {})
return
}
// this part do what ever you want, means every thing allright
}
UPDATE :
You are looking for something wait unit execute "First" and "Second"
in this case use DispatchGroup() here is the example :
var _dispatch_group = DispatchGroup()
getFirestoreData(userID: theUserID) {
_dispatch_group.enter()
print("executes second")
_dispatch_group.leave()
}
_dispatch_group.notify(queue: .main) {
print("executes third")
}
output is :
executes First
executes Second
executes Third

Accept more than one status code range with Alamofirie request

I would like to accept classic status code range (2XX) but also some extra error status code. So, how can i do this with the validate method of Alamofire Request?
Something like that:
Alamofire.request(self)
.validate(statusCode: [ 200..<300 , 403 ])
.responseJSON { response in
switch response.result {
case .Success(let JSON):
...
case .Failure(let error):
...
}
})
Alamofire accepts a Range<Int> parameter with acceptable codes. A range requires that all elements are consecutive, so you'll have to code your own validator. This should work:
.validate { _, response in
let acceptableStatusCodes: [Range<Int>] = [200..<300, 403...403]
if acceptableStatusCodes.map({$0.contains(response.statusCode)}).reduce(false, combine: {$0 || $1}) {
return .Success
} else {
let failureReason = "Response status code was unacceptable: \(response.statusCode)"
return .Failure(Error.errorWithCode(.StatusCodeValidationFailed, failureReason: failureReason))
}
}
You can also declare it in a Request extension for better code reusability:
extension Request {
func validateStatusCode() -> Request {
return self.validate { _, response in
let acceptableStatusCodes: [Range<Int>] = [200..<300, 403...403]
if acceptableStatusCodes.map({$0.contains(response.statusCode)}).reduce(false, combine: {$0 || $1}) {
return .Success
} else {
let failureReason = "Response status code was unacceptable: \(response.statusCode)"
return .Failure(Error.errorWithCode(.StatusCodeValidationFailed, failureReason: failureReason))
}
}
}
}
And call it like this:
Alamofire.request(self)
.validateStatusCode()
.responseJSON { response in
switch response.result {
case .Success(let JSON):
...
case .Failure(let error):
...
}
})