Search code examples
swiftalamofire

Swift "retry" logic on request


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

Solution

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