So i'm a bit lost on how to implement a retry logic when my upload request fail.
Here is my code i would like some guidance on how to do it
func startUploading(failure failure: (NSError) -> Void, success: () -> Void, progress: (Double) -> Void) {
DDLogDebug("JogUploader: Creating jog: \(self.jog)")
API.sharedInstance.createJog(self.jog,
failure: { error in
failure(error)
}, success: {_ in
success()
})
}
Here's a general solution that can be applied to any async function that has no parameters, excepting the callbacks. I simplified the logic by having only success and failure callbacks, a progress should not be that hard to add.
So, assuming that your function is like this:
func startUploading(success: #escaping () -> Void, failure: #escaping (Error) -> Void) {
DDLogDebug("JogUploader: Creating jog: \(self.jog)")
API.sharedInstance.createJog(self.jog,
failure: { error in
failure(error)
}, success: {_ in
success()
})
}
A matching retry function might look like this:
func retry(times: Int, task: #escaping(#escaping () -> Void, #escaping (Error) -> Void) -> Void, success: #escaping () -> Void, failure: #escaping (Error) -> Void) {
task(success,
{ error in
// do we have retries left? if yes, call retry again
// if not, report error
if times > 0 {
retry(times - 1, task: task, success: success, failure: failure)
} else {
failure(error)
}
})
}
and can be called like this:
retry(times: 3, task: startUploading,
success: {
print("Succeeded")
},
failure: { err in
print("Failed: \(err)")
})
The above will retry the startUploading call three times if it keeps failing, otherwise will stop at the first success.
Edit. Functions that do have other params can be simply embedded in a closure:
func updateUsername(username: String, success: #escaping () -> Void, failure: #escaping (Error) -> Void) {
...
}
retry(times: 3, { success, failure in updateUsername(newUsername, success, failure) },
success: {
print("Updated username")
},
failure: {
print("Failed with error: \($0)")
}
)
Update So many #escaping clauses in the retry function declaration might decrease its readability, and increase the cognitive load when it comes to consuming the function. To improve this, we can write a simple generic struct that has the same functionality:
struct Retrier<T> {
let times: UInt
let task: (#escaping (T) -> Void, #escaping (Error) -> Void) -> Void
func callAsFunction(success: #escaping (T) -> Void, failure: #escaping (Error) -> Void) {
let failureWrapper: (Error) -> Void = { error in
// do we have retries left? if yes, call retry again
// if not, report error
if times > 0 {
Retrier(times: times - 1, task: task)(success: success, failure: failure)
} else {
failure(error)
}
}
task(success, failureWrapper)
}
func callAsFunction(success: #escaping () -> Void, failure: #escaping (Error) -> Void) where T == Void {
callAsFunction(success: { _ in }, failure: failure)
}
}
Being callable, the struct can be called like a regular function:
Retrier(times: 3, task: startUploading)(success: { print("success: \($0)") },
failure: { print("failure: \($0)") })
, or can be circulated through the app:
let retrier = Retrier(times: 3, task: startUploading)
// ...
// sometime later
retrier(success: { print("success: \($0)") },
failure: { print("failure: \($0)") })
Here is an updated answer for swift 3. I also added a generic object in the success block so if you make an object after your network call is complete you can pass it along to the final closure. Here is the retry function:
func retry<T>(_ attempts: Int, task: #escaping (_ success: #escaping (T) -> Void, _ failure: #escaping (Error) -> Void) -> Void, success: #escaping (T) -> Void, failure: #escaping (Error) -> Void) {
task({ (obj) in
success(obj)
}) { (error) in
print("Error retry left \(attempts)")
if attempts > 1 {
self.retry(attempts - 1, task: task, success: success, failure: failure)
} else {
failure(error)
}
}
}
And here is how you would use it if you updated a user and wanted to get back a new user object with the updated info:
NetworkManager.shared.retry(3, task: { updatedUser, failure in
NetworkManager.shared.updateUser(user, success: updatedUser, error: failure) }
, success: { (updatedUser) in
print(updatedUser.debugDescription)
}) { (err) in
print(err)
}
Updated to swift 5, with Result type instead of success and failure blocks.
func retry<T>(_ attempts: Int, task: #escaping (_ completion:#escaping (Result<T, Error>) -> Void) -> Void, completion:#escaping (Result<T, Error>) -> Void) {
task({ result in
switch result {
case .success(_):
completion(result)
case .failure(let error):
print("retries left \(attempts) and error = \(error)")
if attempts > 1 {
self.retry(attempts - 1, task: task, completion: completion)
} else {
completion(result)
}
}
})
}
This is how we can use the retry function:
func updateUser(userName: String) {
retry(3, task: { (result) in
startUploadingWithResult(userName: userName, completion: result)
}) { (newResult) in
switch newResult {
case .success(let str):
print("Success : \(str)")
case .failure(let error):
print(error)
}
}
}
updateUser(userName: "USER_NAME")
Related
I have a method execute that calls an external API with a callback that receives Result<Data?,Error>. How can I map that optional success to an unwrapped result or an Error?
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
handle(mappedResult)
}
}
This code fails with Invalid conversion from throwing function of type '(Optional<Data>) throws -> _' to non-throwing function type '(Data?) -> NewSuccess'
I was able to do this with a simple switch (below), but I was wondering if throwing a failure inside the .map is possible.
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
switch result {
case .failure(let error):
handle(.failure(error))
case .success(let data):
guard let data = data else {
handle(.failure(NSError(domain: "", code: 0, description: "error")))
return
}
handle(.success(data))
}
}
}
You can convert between throws functions and functions that return Result<Success, Error> by using Result(catching:) and .get().
Here's your original map call:
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
Result.map takes a Result and a function that converts (Success) -> NewSuccess, and returns a Result<NewSuccess, Failure>.
Your map takes a Data (Success), and returns Result<Data, Error> (NewSuccess). So the final type, by plugging in NewSuccess is: Result<Result<Data, Error>, Error>. That's more layers than you want. You want to flatten that to just Result<Data, Error>, and that's where flatMap comes in.
Your answer shows that, but you can also pull this out into a more general-purpose tool. It only works when Failure == Error, because throws is untyped, so you can't limit it to some subset of errors. But that's what you're doing anyway. Here's tryMap:
extension Result where Failure == Error {
func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> {
self.flatMap { value in
Result<NewSuccess, Error> { try transform(value) }
}
}
}
With that, you can rewrite this as:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.tryMap {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
})
}
}
That said, I'd probably be tempted to write it this way:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap { maybeData in
maybeData.map(Result.success)
?? .failure(NSError(domain: "", code: 0, description: "error"))
})
}
}
Or if I wanted someone to be able to actually read it later:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap {
switch $0 {
case .some(let data): return .success(data)
case .none: return .failure(NSError(domain: "", code: 0, description: "error"))
}
}
)
}
}
The advantage of this switch over yours is that you don't have to unwrap and rewrap previous failures.
Apparently, this can be done using flatmap. So in my case:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.flatMap { data in
Result<Data, Error> {
guard let data = data else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
}
handle(mappedResult)
}
}
It's a little confusing, but it is working for me.
how fix it? How should I use do-try-catch correctly.
This is possible when I remove the error code from the catch segment, but I need the specific type of error, so how do I catch a specific error and use it?
static func request<T: HandyJSON>(
target: StatusAPI,
type: T.Type,
success successCallback: (T) -> Void,
error errorCallback: (_ statusCode: Int) -> Void,
failure failureCallback: #escaping (MoyaError) -> Void
) {
provider.request(target) { result in
switch result {
case let .success(response):
do {
try response.filterSuccessfulStatusCodes()
} catch {
print(error)
}
case let .failure(error):
failureCallback(error)
}
}
}
I heard that I can get out of callback hell using RxSwift.
But I don't have an idea how to improve callback hell.
The samples below need to be called in the order getItem1() -> getItem2() -> getItem3()
Is there a way out of this callback hell using RxSwift?
class MyItem {
// MARK: - Public
// Callback hell
public func getMyItem(success: #escaping (String) -> Void, failure: #escaping (Error) -> Void) {
self.getItem1(success: { [weak self] item1 in
self?.getItem2(item1: item1, success: { [weak self] item2 in
self?.getItem3(item2: item2, success: { item3 in
success(item3)
}, failure: { err3 in
print(err3)
failure(err3)
})
}, failure: { err2 in
print(err2)
failure(err2)
})
}, failure: { err1 in
print(err1)
failure(err1)
})
}
// MARK: - Private
private func getItem1(success: #escaping (String) -> Void, failure: #escaping (Error) -> Void) {
// Request to sever
}
private func getItem2(item1: String, success: #escaping (String) -> Void, failure: #escaping (Error) -> Void) {
// Request to sever
}
private func getItem3(item2: String, success: #escaping (String) -> Void, failure: #escaping (Error) -> Void) {
// Request to sever
}
}
I'm waiting for your answer to teach me.
Your sample, using RxSwift, would look like this:
class MyItem {
// MARK: - Public
public func getMyItem() -> Observable<String> {
getItem1()
.flatMap(getItem2(item1:))
.flatMap(getItem3(item2:))
.do(onError: { print($0) })
}
}
private func getItem1() -> Observable<String> {
// Request to server
}
private func getItem2(item1: String) -> Observable<String> {
// Request to server
}
private func getItem3(item2: String) -> Observable<String> {
// Request to server
}
There are seven different systems used in iOS code to push data from one object to another:
closures
target/action (UIControl aka IBAction)
delegates
notifications (NotificationCenter)
KVO
setting variables
sub-classing abstract base classes
A lot of the complexity of writing an application comes from trying to integrate these various systems into a unified whole. Each of the systems individually is simple to use, but weaving them together makes our code complex. One of the benefits of using RxSwift is that it wraps all of the above systems into a single powerful mechanism, thus making our code less complex overall.
Knowing the above gives a clue on how to go about integrating RxSwift into an existing project. If RxSwift can replace all those other technologies, then to integrate means to replace them with it. Once this is done, we will be able to make the code more declarative and less complex.
From Integrating RxSwift into Your Brain and Code Base
If for some reason you don't want to change the signatures of any of the functions you have, then you could do something crazy like this:
public func getMyItem(success: #escaping (String) -> Void, failure: #escaping (Error) -> Void) {
_ = rx_(self.getItem1)
.flatMap(rx_(self.getItem2))
.flatMap(rx_(self.getItem3))
.do(onError: { print($0) })
.subscribe(onNext: success, onError: failure)
}
The above uses the following support functions (all of these are free functions. Do not put them in a class.)
func rx_<A, B>(_ fn: #escaping (A, #escaping (B) -> Void, #escaping (Error) -> Void) -> Void) -> (A) -> Observable<B> {
{ input in
.create(observe(curry(fn, input)))
}
}
func rx_<A>(_ fn: #escaping (#escaping (A) -> Void, #escaping (Error) -> Void) -> Void) -> Observable<A> {
.create(observe(fn))
}
func curry<A, B>(_ fn: #escaping (A, #escaping (B) -> Void, #escaping (Error) -> Void) -> Void, _ a: A) -> (#escaping (B) -> Void, #escaping (Error) -> Void) -> Void {
{ success, failure in
fn(a, success, failure)
}
}
func observe<A>(_ fn: #escaping (#escaping (A) -> Void, #escaping (Error) -> Void) -> Void) -> (AnyObserver<A>) -> Disposable {
{ observer in
fn(singleObserve(observer), observer.onError)
return Disposables.create()
}
}
func singleObserve<A>(_ observer: AnyObserver<A>) -> (A) -> Void {
{ element in
observer.onNext(element)
observer.onCompleted()
}
}
I don't recommend this sort of thing for production code. IMO, something like the above trades callback hell for abstraction hell, but it's a fun exercise.
I have this piece of code in my app:
func saveContact2(contact: String) throws {
let contactStore = CNContactStore()
contactStore.requestAccess(for: .contacts, completionHandler: {(granted, error) in
if granted && error == nil {
//...
} else {
if !granted {
throw contactErrors.contactAccessNotGranted(["Error","Access to Contacts is not granted."])
}
}
})
}
I'd like to throw all errors raising in closure to calling function.
Compiler shows error:
Invalid conversion from throwing function of type '(_, _) throws -> ()' to non-throwing function type '(Bool, Error?) -> Void'
Could anyone help me please with the right syntax?
You cannot throw errors from an #escaping closure that is called asynchronously. And this makes sense because your app has carried on with its execution and there’s no where to catch the error.
So, instead, adopt completion handler pattern yourself:
func saveContact2(_ contact: String, completion: #escaping: (Result<Bool, Error>) -> Void) {
let contactStore = CNContactStore()
contactStore.requestAccess(for: .contacts) { (granted, error) in
guard granted else {
completion(.failure(error!)
return
}
//...
completion(.success(true))
}
}
And then you’d call it like:
saveContact2(contactName) { result in
switch result {
case .failure:
// handler error here
case .success:
// handle confirmation of success here
}
}
If you’re using an old compiler that doesn’t have the Result type, it’s basically:
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}
I've been trying to figure out how to upload multiple files (images) to AWS S3 from Swift. I asked a previous question here but did not receive any responses. Rather than try & ask the same question again, I'm hoping to find guidance on creating a retry function/wrapper for a function that fires a network request w/ a completion handler. I found this question here & have tried to model it similarly. The answers by Cristik & JustinM are written in Swift 3, so when trying to write them in Swift 4, I had to make some changes. Sadly, I'm worried I lost some logic in the conversion (I don't fully understand everything that they suggested, so some of my changes were just suggestions by Xcode). Here is what I have refactored from their question & implementing the AWS S3 Transfer Utility:
static func retryFunction(numberOfTimes: Int, task: #escaping (_ success: # escaping (String) -> (), _ failure: #escaping (Error) -> ()) -> (), success: #escaping (String) -> (), failure: #escaping (Error) -> ()) {
task({ (result) in
success(result)
}) { (error) in
if numberOfTimes > 1 {
print("retrying with \(numberOfTimes - 1)")
self.retryFunction(numberOfTimes: numberOfTimes - 1, task: task, success: success, failure: failure)
} else {
failure(error)
}
}
}
static func singleImageUploadAttempt(_ dataToUpload: Data, _ imageKey: String,
_ success: #escaping (String) -> (), _ failure: #escaping (Error) -> ()) {
let expression = AWSS3TransferUtilityUploadExpression()
var completionHandler: AWSS3TransferUtilityUploadCompletionHandlerBlock?
let transferUtility = AWSS3TransferUtility.default()
completionHandler = { (task, error) -> Void in
if let error = error {
print("failure to upload the image: \(imageKey)")
failure(error)
} else{
print("\(imageKey) uploaded successfully")
success(imageKey)
}
}
transferUtility.uploadData(dataToUpload, bucket: s3BucketName, key: imageKey, contentType: "image/png",
expression: expression, completionHandler: completionHandler).continueWith { (task) -> AnyObject! in
if let error = task.error {
print("An error occurred generating the task")
failure(error)
}
return nil;
}
}
static func uploadImages(imagesToUpload: [UIImage], complete: #escaping (String?) -> ()) {
let folderKey = UUID().uuidString
var retryArray = [String: Data]()
let imageGroup = DispatchGroup()
for i in 0..<imagesToUpload.count {
let resizedImage = compressImage(imageToReduce: imagesToUpload[i], expectedSizeInMb: 2)
let imageKey = "public/\(folderKey)/image\(i).png"
let imageData = UIImagePNGRepresentation(resizedImage!)
retryArray[imageKey] = imageData!
imageGroup.enter()
retryFunction(numberOfTimes: 3, task: { success, failure in singleImageUploadAttempt(imageData!, imageKey,
success, failure) },
success: { (keyToRemove) in
retryArray.removeValue(forKey: keyToRemove)
print("updated")
print("successfully uploaded \(imageKey)")
imageGroup.leave()
},
failure: { (err) in
print("failed to upload \(imageKey)")
imageGroup.leave()
}
)
}
imageGroup.notify(queue: DispatchQueue.main) {
if(retryArray.keys.count > 0) {
print("the images could not be uploaded")
complete(nil)
} else {
print("all images uploaded successfully")
complete(folderKey)
}
}
}
Right now, the image upload works fine when I have a connection. When I try without a connection, however, the function just waits infinitely. It calls the singleImageUploadAttempt for each image but doesn't hit the completion handler or task.error. None of my print statements are fired, and rather than hitting the function 3 times for each image (3 retries), it hits the imageGroup.notify() and then just does nothing. I apologize if I misspoke on any of this - I'm still trying to understand all of this Swift logic/syntax. I appreciate any help/suggestions, thank you!