How to retry URL requests indefinitely until success with Alamofire 5? - ios

Is there any recommendation on using Alamofire's RequestRetrier to keep retrying a request until it succeeds?
The issue I'm running into is every time a retry happens it increases the memory of the app. I have a scenario that a user might make a request offline and it should eventually succeed when they regain connection.
Is it not good practice to continually retry a request? Should I use a Timer instead to to recreate the network call if it fails a few times? Should I use some other mechanism that waits for network activity and then sends up a queue of requests (any suggestions on what that would be would be appreciated!)?
Here is an example to see the memory explode. You just need to put your iPhone in airplane mode (so there is no connection) and the memory will keep going up rapidly since the call will happen a lot in this case.
import UIKit
import Alamofire
class ViewController: UIViewController {
let session = Session(configuration: .default)
override func viewDidLoad() {
super.viewDidLoad()
sendRequest()
}
func sendRequest() {
session.request("https://www.google.com", method: .get, interceptor: RetryHandler())
.responseData { (responseData) in
switch responseData.result {
case .success(let data):
print(String(data: data, encoding: .utf8) ?? "nope")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
class RetryHandler: RequestInterceptor {
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
// keep retrying on failure
completion(.retry)
}
}(

Related

How to set up completion handlers in API Call - Swift 5

I've run into a problem.
I've made a service for API calls that returns a User object, and the API call works fine and it returns the User object.
However, I need to use completion and #escaping, and I'm not sure how to do it properly, since I'm new to Swift and iOS development.
The Code is below.
func login(with credentials: LoginCredentials) {
let params = credentials
AF.request(Service.baseURL.appending("/user/login"), method: .post, parameters: params, encoder: JSONParameterEncoder.default).responseJSON { response in
switch response.result {
case .success:
print("Success")
case.failure:
print("Fail")
}
}
Also, how do I call a function when there is completion?
It’s not clear what kind of argument (if it takes one) the completion for your method shall take, thus I’m gonna give you a couple of hints.
Perhaps you want to also have the completion for your method execute on a queue specified as parameter in the call.
Therefore you might structure your method’s signature in this way:
func login(with credentials: LoginCredentials, queue: DispatchQueue = .main, _ completion: #esacping (Bool) -> Void)
Here I’ve used the type Bool as argument for the completion to be executed, and defaulted the queue argument to be .main, assuming that you’re just interested in deliver as result argument in the completion a value of true in case you’ve gotten a .success response, false otherwise; I’m also assuming that this method will mainly be called with the completion being executed on the main thread for updating a state, hence the default value for the queue argument.
Therefore your method body could become like this:
func login(with credentials: LoginCredentials, queue: DispatchQueue = .main, _ completion: #escaping (Bool) -> Void) {
let params = credentials
AF.request(
Service.baseURL.appending("/user/login"),
method: .post,
parameters: params,
encoder: JSONParameterEncoder.default
).responseJSON { response in
switch response.result {
case .success:
queue.async { completion(true) }
case.failure:
queue.async { completion(false) }
}
}
Now of course if you need to deliver something different (as in the token for the login session and an error in case of failure) then you’d better adopt a Result<T,Error> Swift enum as argument to pass to the completion.
Without you giving more details in these regards, I can only give a generic suggestion for this matter, cause I cannot go into the specifics.
EDIT
As a bonus I'm also adding how you could structure this method with the new Swift concurrency model:
// Assuming you are returning either a value of type User or
// throwing an error in case the login failed (as per your comments):
func login(with credentials: LoginCredentials) async throws -> User {
withCheckedThrowingContinuation { continuation in
AF.request(
Service.baseURL.appending("/user/login"),
method: .post,
parameters: credentials,
encoder: JSONParameterEncoder.default
).responseJSON { response in
switch response.result {
case .success:
let loggedUser = User(/* your logic to create the user to return*/)
continuation.resume(returning: loggedUser)
case.failure:
continuation.resume(throwing: User.Error.loginError)
}
}
}
// Here's possible way to implement the error on your User type
extension User {
enum Error: Swift.Error {
case loginError
// add more cases for other errors related to this type
}
}

Network request intermittently failing with error -1009 The internet connection appears to be offline

I have multiple request that I call on the viewDidLoad method of my initial view controller. Sometimes these requests are failing with Error Code -1009 The internet connection appears to be offline. for some user and others are not having it. Sometimes users who get the error can use it after sometime with no issues. This usually happens on a cellular network. And other apps are working in their phone and not ours.
All of these requests are using the same method on my Service class. First I have an enum of the APIs and I convert them to a URLRequest using Alamofire's URLRequestConvertible protocol. Then I pass this request into Alamofire and handle the response.
func get<T:Codable>(_ api: ServiceAPI, resultType: T.Type, completion: #escaping (_ result: T?, _ error: Error?) -> ()) {
Alamofire.request(api).validate().responseJSON { response in
switch response.result {
case .success(let json):
print("\(api.path):\n\(json)")
do {
if let data = response.data {
let jsonDecoder = JSONDecoder()
let dictionary = try jsonDecoder.decode([String: T].self, from: data)
if let result = dictionary["result"] {
completion(result, nil)
} else {
completion(nil, self.resultNotFoundError)
}
}
} catch {
completion(nil, error)
}
case .failure(let error):
error.trackMixPanelEvent()
completion(nil, error)
}
}
}
Since I am creating an instance of the Service class and calling the get method on it for each request, is it possible that the request is being deallocated? What could also be the cause of such intermittent network error?
To also point out, all of the web service requests are using POST method.

What is the better way to call multiple services in iOS?

I have 5 different services requests to load into same UItableView for each cells.
What is the best way to approach in order to do this.
https://example.com/?api/service1
https://example.com/?api/service2
https://example.com/?api/service3
https://example.com/?api/service4
https://example.com/?api/service5
let url = "https://example.com/?api/service1
Alamofire.request(url, method: .get, parameters:nil encoding: JSONEncoding.default, headers: nil)
.responseJSON { response in
print(response.result.value as Any) // result of response serialization
}
repeat the same Alamofire five times with different services name there is another way to implement it.
Look at using a DispatchGroup to perform multiple async requests and wait for them all to complete.
For each task you call group.enter() and in its completion handler when you know that request has finished you call group.leave(). Then there is a notify method which will wait for all requests to call leave to tell you that they have all finished.
I have created an example in a Playground (which will fail with errors because of the URL's used)
import UIKit
import PlaygroundSupport
let serviceLinks = [
"https://example.com/?api/service1",
"https://example.com/?api/service2",
"https://example.com/?api/service3",
"https://example.com/?api/service4",
"https://example.com/?api/service5"
]
// utility as I've not got alamofire setup
func get(to urlString: String, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
let url = URL(string: urlString)!
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
}
let group = DispatchGroup()
for link in serviceLinks {
group.enter() // add an item to the list
get(to: link) { data, response, error in
// handle the response, process data, assign to property
print(data, response, error)
group.leave() // tell the group your finished with this one
}
}
group.notify(queue: .main) {
//all requests are done, data should be set
print("all done")
}
PlaygroundPage.current.needsIndefiniteExecution = true
You probably won't be able to just loop through the URL's like I have though because the handling of each service is probably different. You'll need to tweak it based on your needs.
There is alot more information about DispatchGroups available online such as this article

Alamofire not failing when airplane mode is on

I'm developing an iOS download app, that uses Alamofire 4.4 and Swift 3.0.
The app gets a url and downloads the file using Alamofire.
I'm using the background session manager so that the app can download whilst in the background and send a notification when complete.
For some reason, after adding this, the .failure response is never triggered even when putting airplane mode on.
If it turn airplane mode back off, the download weirdly continues going, but leave it long enough and it doesn't continue downloading, so you'd imagine it would have triggered the failure...
I have this code for the download request:
if let resumeData = resumeData {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(resumingWith: resumeData, to: destination)
}
else {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(extUrl, to: destination)
}
alamoRequest = request
.responseData { response in
switch response.result {
case .success:
//get file path of downloaded file
let filePath = response.destinationURL?.path
completeDownload(filePath: filePath!)
case .failure:
//this is what never calls on network drop out
print("Failed")
}
.downloadProgress { progress in
let progressPercent = Float(progress.fractionCompleted)
//handle other progress stuff
}
}
.success case triggers fine, and progress returns fine.
If I cancel a download with alamoRequest?.cancel() then it does trigger the .failure case.
Here's my completion handler:
class BackendAPIManager: NSObject {
static let sharedInstance = BackendAPIManager()
var alamoFireManager : Alamofire.SessionManager!
var backgroundCompletionHandler: (() -> Void)? {
get {
return alamoFireManager?.backgroundCompletionHandler
}
set {
alamoFireManager?.backgroundCompletionHandler = newValue
}
}
fileprivate override init()
{
let configuration = URLSessionConfiguration.background(withIdentifier: "com.uniqudeidexample.background")
self.alamoFireManager = Alamofire.SessionManager(configuration: configuration)
}
}
And in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackendAPIManager.sharedInstance.backgroundCompletionHandler = completionHandler
//do other notification stuff...
}
Before I added the background session stuff it worked ok, and when I activated airplane mode it failed. So am I missing something?
Hope that all makes sense, it's my first app, so not clued up on all the technicalities,
Thanks

HTTP requests being blocked by a previous one using Alamofire

I'm having some trouble with an HTTP request performed from our iOS app to our API. The problem with this request is that it usually takes 30-40s to complete. I don't need to handle the response for now, so I just need to fire it and forget about it. I don't know if the problem is in my code or in the server, that's why I'm asking here.
I'm using Alamofire and Swift 2.2 and all the other requests are working just fine. This is an screenshot from Charles proxy while I was trying to debug it: Charles screenshot
As you can see, the request that blocks the others is the refreshchannels. When that request fires (#6 and #25), the others are blocked and don't finish until the refreshchannels finishes.
Here is the code that triggers that request and also the APIManager that i've built on top of Alamofire:
// This is the method that gets called when the user enables the notifications in the AppDelegate class
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
// Recieve the APNSToken and handle it. I've removed it to make it shorter
// This sends a POST to our API to store some data
APIManager().registerForPushNotifications(parametersPush) { (result) in
switch result {
case .Success (let JSON):
// This is the slow call that blocks the other HTTP requests
APIManager().refreshChannels { _ in } // I don't need to handle the response for now
case .Failure: break
}
}
}
And the manager:
//This is my custom manager to handle all the networking inside my app
class APIManager {
typealias CompletionHandlerType = (Result) -> Void
enum Result {
case Success(AnyObject?)
case Failure(NSError)
}
let API_HEADERS = Helper.sharedInstance.getApiHeaders()
let API_DOMAIN = Helper.sharedInstance.getAPIDomain()
//MARK: Default response to a request
func defaultBehaviourForRequestResponse(response: Response<AnyObject, NSError>, completion: CompletionHandlerType) {
print("Time for the request \(response.request!.URL!): \(response.timeline.totalDuration) seconds.")
switch response.result {
case .Success (let JSON):
if let _ = JSON["error"]! {
let error = NSError(domain: "APIError", code: response.response!.statusCode, userInfo: JSON as? [NSObject : AnyObject])
completion(Result.Failure(error))
} else {
completion(Result.Success(JSON))
}
case .Failure (let error):
completion(Result.Failure(error))
}
}
func refreshChannels(completion: CompletionHandlerType) {
Alamofire.request(.PUT, "\(API_DOMAIN)v1/user/refreshchannels", headers: API_HEADERS).responseJSON { response in
self.defaultBehaviourForRequestResponse(response, completion: completion)
}
}
}
Any help will be appreciated. Have a nice day!

Resources