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!
Related
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
}
}
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)
}
}(
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.
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
I was wanting to know if anyone knows how to detect if there is data transfer or not within an iOS app. I am using a Swift Reachability library to detect if there is wifi or cellular connection, but this does not let me know if there is actual data being transferred.
My app starts out with a WKWebView, and in the case there is no data being transferred to load the webview that takes up the entire screen, I would like to present an alertController to the user.
Reachability lets me know if there is a connection to wifi or a cellular network, but they don't help with letting me know if there is any data being transferred. I'm testing with my wifi on, but with no network connection, and I'm unable to present any alertController as connection is always passing true for isReachable().
Does anyone know how to go about this? Thank you.
Alamofire Provide the functionaly to check the network status. So here it is in detail with some other functionality.
I have created an APIClient Manager class in my project.
import UIKit
import Alamofire
import AlamofireNetworkActivityIndicator
// Completion handeler
typealias TSAPIClientCompletionBlock = (response: NSHTTPURLResponse?, result: AnyObject?, error: NSError?) -> Void
// MARK: -
class TSAPIClient: Manager {
// MARK: - Properties methods
private let manager = NetworkReachabilityManager(host: "www.apple.com") // could be any website, Just to check connectivity
private var serviceURL: NSURL?
// MARK: - init & deinit methods
init(baseURL: NSURL,
configuration: NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration(),
delegate: SessionDelegate = SessionDelegate(),
serverTrustPolicyManager: ServerTrustPolicyManager? = nil) {
super.init(configuration: configuration, delegate: delegate, serverTrustPolicyManager: serverTrustPolicyManager)
var aURL = baseURL
// Ensure terminal slash for baseURL path, so that NSURL relativeToURL works as expected
if aURL.path?.characters.count > 0 && !aURL.absoluteString.hasSuffix("/") {
aURL = baseURL.URLByAppendingPathComponent("")
}
serviceURL = baseURL
NetworkActivityIndicatorManager.sharedManager.isEnabled = true
observeNetworkStatus()
}
// MARK: - Public methods
func serverRequest(methodName: String, parameters: [String : AnyObject]? , isPostRequest: Bool, headers: [String : String]?, completionBlock: TSAPIClientCompletionBlock) -> Request {
let url = NSURL(string: methodName, relativeToURL: serviceURL)
print("\(url)")
let request = self.request(isPostRequest ? .POST : .GET, url!, parameters: parameters, encoding: isPostRequest ? .JSON : .URL, headers: headers)
.validate(statusCode: 200..<600)
.responseJSON { response in
switch response.result {
case .Success:
completionBlock(response: response.response, result: response.result.value, error: nil
)
break
case .Failure(let error):
completionBlock(response: response.response, result: nil, error: error)
break
}
}
return request
}
func cancelAllRequests() {
session.getAllTasksWithCompletionHandler { tasks in
for task in tasks {
task.cancel()
}
}
}
// this will contiously observe network changes
func observeNetworkStatus() {
manager?.listener = { status in
print("Network Status Changed: \(status)")
if status == .NotReachable {
} else if status == .Unknown {
} else {
// status is reachable
// posting a notification for network connectivity NSNotificationCenter.defaultCenter().postNotificationName(kNetworkStatusConnectedNotification, object: nil)
}
}
manager?.startListening()
}
// If you want to check network manually for Example before pushing a viewController.
func isNetworkReachable() -> Bool {
return manager?.isReachable ?? false
}
}
kNetworkStatusConnectedNotification is a constant declared as following
let kNetworkStatusConnectedNotification = "kNetworkStatusConnectedNotification"
Now, Where you want to observe the network changes in your application, register the notification in that viewController as follows
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeViewController.applicationIsConnectedToNetwok), name: kNetworkStatusConnectedNotification, object: nil)
and in the selector do whatever you want to do when you get network connectivity
func applicationIsConnectedToNetwok() {
// e.g LoadData
}
I have written this method in my Session class and I am using it to check connectivity mannualy
func isNetworkReachable() -> Bool {
return YourAPIClient.sharedClient.isNetworkReachable()
}
I have added some comments in the code for your understanding. I hope it helps.