This has got to be one of the strangest thing I've come across...
Simple network request using alamofire
manager.request(PostRouter.readPosts(pn: pn)).validate().responseJSON {
response in
switch response.result {
case .success(let json):
if let json = json as? JSON,
let postsArray = json["posts"] as? [JSON],
let posts = Post.build(from: postsArray) {
completion(posts, nil)
}
else {
completion(nil, NetworkingError.jsonParsingError)
}
case .failure(let error):
completion(nil, error)
}
}
The same thing happens whether running from the simulator or mobile.
make request in airplane mode -> error of no internet connection.
make request again with internet -> loads posts
make request again in airplane mode -> loads posts without internet. Returns a success case with json value.
Even if in step 3 I make the request with an internet connection and have since modified the value of the data between steps 2 and 3, it will still return the exact same data from step 2 instead of fetching new data. It's almost like the whole request, including the data is saved in memory.
The only way I can actually make a new request to fetch the new data is by removing it from my phone and installing it again. The issue then repeats itself.
This happens to all the requests I make from the app. Also, The issue isn't linked to Alamofire because without it the issue still occurs
It's probably not an issue but a feature. Foundation will automatically cache responses. Check the response's cache headers (like Expires, Cache-Control, ETag etc.) if the server enables caching.
Related
I am currently developing an application for iOS. Most of the features that I wanted implemented I have already finished, but there is one feature in particular that I really need to have - Network Errors handling.
So for example: A user is trying to refresh his data inside my application. Instead of my app crashing or simply not doing anything, I would love for that exception to be caught, identified and then display a corresponding message on screen using AlertDialogs. for example:
Network Error - title;
Unreachable host, please check your network connectivity and try again - Message;
OK - button;
I was able to have this working in my Android application and it's quite useful, however, I am quite new to Swift and iOS development, so, please help me out here and point me in the right direction.
I am currently using latest Alamofire for sending HTTP Requests, here is my example of HTTP Request that I have implemented inside my application.
func loadProfile() {
let url = Constants.profileURL
let headers: HTTPHeaders = ["Cookie": "username; password"]
AF.request(url, method: .post, headers: headers).response {response in
if let data = response.data, let dataString = String(data: data, encoding: .utf8) {
if dataString.contains(Constants.loginSuccess) {
//Do something
}
}
}
}
Alamofire's DataResponse (and all response types) contain the Result of serialization. You can use that value to check whether serialization, and the request in general, succeeded or failed. You can then pass the result value to completion handlers to be dealt with as you wish.
I'm fairly new to iOS/Swift development and I'm working on an app that makes several requests to a REST API. Here's a sample of one of those calls which retrieves "messages":
func getMessages() {
let endpoint = "/api/outgoingMessages"
let parameters: [String: Any] = [
"limit" : 100,
"sortOrder" : "ASC"
]
guard let url = createURLWithComponents(endpoint: endpoint, parameters: parameters) else {
print("Failed to create URL!")
return
}
do {
var request = try URLRequest(url: url, method: .get)
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
if let error = error {
print("Request failed with error: \(error)")
// TODO: retry failed request
} else if let data = data, let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
// process data here
} else {
// TODO: retry failed request
}
}
}
task.resume()
} catch {
print("Failed to construct URL: \(error)")
}
}
Of course, it's possible for this request to fail for a number of different reasons (server is unreachable, request timed out, server returns something other than 200, etc). If my request fails, I'd like to have the ability to retry it, perhaps even with a delay before the next attempt. I didn't see any guidance on this scenario in Apple's documentation but I found a couple of related discussions on SO. Unfortunately, both of those were a few years old and in Objective-C which I've never worked with. Are there any common patterns or implementations for doing something like this in Swift?
This question is airing on the side of opinion-based, and is rather broad, but I bet most are similar, so here goes.
For data updates that trigger UI changes:
(e.g. a table populated with data, or images loading) the general rule of thumb is to notify the user in a non-obstructing way, like so:
And then have a pull-to-refresh control or a refresh button.
For background data updates that don't impact the user's actions or behavior:
You could easily add a retry counter into your request result depending on the code - but I'd be careful with this one and build out some more intelligent logic. For example, given the following status codes, you might want to handle things differently:
5xx: Something is wrong with your server. You may want to delay the retry for 30s or a minute, but if it happens 3 or 4 times, you're going to want to stop hammering your back end.
401: The authenticated user may no longer be authorized to call your API. You're not going to want to retry this at all; instead, you'd probably want to log the user out so the next time they use your app they're prompted to re-authenticate.
Network time-out/lost connection: Retrying is irrelevant until connection is re-established. You could write some logic around your reachability handler to queue background requests for actioning the next time network connectivity is available.
And finally, as we touched on in the comments, you might want to look at notification-driven background app refreshing. This is where instead of polling your server for changes, you can send a notification to tell the app to update itself even when it's not running in the foreground. If you're clever enough, you can have your server repeat notifications to your app until the app has confirmed receipt - this solves for connectivity failures and a myriad of other server response error codes in a consistent way.
I'd categorize three methods for handling retry:
Reachability Retry
Reachability is a fancy way of saying "let me know when network connection has changed". Apple has some snippets for this, but they aren't fun to look at — my recommendation is to use something like Ashley Mill's Reachability replacement.
In addition to Reachability, Apple provides a waitsForConnectivity (iOS 11+) property that you can set on the URLSession configuration. By setting it, you are alerted via the URLSessionDataDelegate when a task is waiting for a network connection. You could use that opportunity to enable an offline mode or display something to the user.
Manual Retry
Let the user decide when to retry the request. I'd say this is most commonly implemented using a "pull to refresh" gesture/UI.
Timed/Auto Retry
Wait for a few second and try again.
Apple's Combine framework provides a convenient way to retry failed network requests. See Processing URL Session Data Task Results with Combine
From Apple Docs: Life Cycle of a URL Session (deprecated)... your app should not retry [a request] immediately, however. Instead, it should use reachability APIs to determine whether the server is reachable, and should make a new request only when it receives a notification that reachability has changed.
I would like to pull data from a Facebook event page that would fetch all the events listed by a Facebook page. I have to up the iOS SDK, but i am now unsure how to call the Graph API. I found the PAGE ID for a Facebook group page i want to take the events from by looking at the page's source code and finding "PAGEID".
I have set it up all in my dashboard along with my Bundle ID. The only left is to call the Graph API, but i am unsure and left confused when trying to following their doc.
Show us the code you tried already and what is not working.
Have you tried this already? (where {page-id} is your page's ID):
let connection = GraphRequestConnection()
connection.add(GraphRequest(graphPath: "/{page-id}/events")) { httpResponse, result in
switch result {
case .success(let response):
print("Graph Request Succeeded: \(response)")
case .failed(let error):
print("Graph Request Failed: \(error)")
}
}
connection.start()
I'm just exploring using Alamofire and it is excellent but I'd like to do something that I feel is possible just not sure how.
Our authentication with the server uses one-time-use bearer tokens. So for every request made I have to store the new token sent down with that request.
What I'd like to do is intercept every response that comes back and check the Authorisation header. Save it to disk and then forward to the place waiting for the actual data.
Is this possible with Alamofire?
If so, please could you point me in the right direction.
Thanks
OK, after a bit of searching the github and head scratching I decided to create a new response serialiser by extending the Request type.
I created a new saveAuth() block like so...
extension Request {
public static func AuthSaver() -> ResponseSerializer<Bool, NSError> {
return ResponseSerializer { request, response, data, error in
guard error == nil else { return .Failure(error!) }
if let auth = response?.allHeaderFields["Authorization"] as? String {
Router.OAuthToken = auth // this uses a didset on the Router to save to keychain
}
return .Success(true)
}
}
public func saveAuth() -> Self {
return response(responseSerializer: Request.AuthSaver()) {_ in}
}
}
I can call it like...
Alamofire.request(Router.Search(query: query))
.validate()
.responseSaveAuth() // this line
.responseJSON {
response in
// ...
}
It still requires adding in each place that I want to strip out the newly sent auth token but it means I can choose when not to do it also and it's a single line of code.
It's maybe not the most elegant code in the extension (I'm still getting to grips with it all) but it makes it much easier to save the authentication each time.
I have solved this by only having one place in my app that sends network requests. Basically, I have a "network manager" that builds up NSURLRequests and pipes them to one function that actually sends the request (in my case it's an NSOperation sub class). That way I have only one location that I'm reading responses from.
I've ran into a bit of a performance issue with my iOS app, this is my first time working with NSURLSession and NSURLRequest, and although I've tried to inform myself as much as I can, I've hit a wall trying to debug a performance issue I'm facing.
So here's what I got: I've got an iOS 9 app written in Swift 2, I'm communicating with a NodeJS/Express server through Get, Post and Put Http requests, utilizing NSURLRequest and NSURLMutableRequest. I'm sending requests to fetch a group of objects (All together no more than 12000 bytes), however the requests are taking a significant amount of time (sometimes up to a minute). I've added logging to the nodeJs server and I can see that the requests take no longer than 30 milliseconds to be processed.
Note: I'm unsure if this is relevant, but I'm using a singleton "helper "class to make all my api requests and parse the results (saving authentication tokens, parsing JSON objects and saving them to Core Data, saving user preferences to NSUserDefaults, etc), I'm using a singleton so i can access it statically and I'm parsing all the data without saving anything in singleton's properties other than the URL of the server and the NSURLSession.
Here's what my code looks like.
//On initialization of the helper class
private let session = NSURLSession.sharedSession()
func getAllObjects() {
let route = "api/someRoute"
let request = getRequest(route)
request.timeoutInterval = httpTimeout
session.dataTaskWithRequest(request, completionHandler: ResultingObjects).resume()
}
The getRequest method returns a formatted NSMutableURLRequest, shown
here:
func getRequest(route: String) -> NSMutableURLRequest {
let request = NSMutableURLRequest()
request.URL = NSURL(string: "\(serverUrl)/\(route)")!
request.HTTPMethod = "GET"
request.addValue("Bearer \(self.AuthenticationToken()!)", forHTTPHeaderField: "Authorization")
return request
}
The completion handler will parse the objects returned and notify
the main thread with the resulting parsed objects, as so:
private func ResultingObjects(data: NSData?, response: NSURLResponse?, error: NSError?) {
if let d = data {
if !isAuthorized(d){
return
}
do {
if let JSON = try NSJSONSerialization.JSONObjectWithData(d, options: []) as? NSDictionary {
if let message = JSON["message"] as? String {
if message == "Empty result" {
//- Return notification to be handled in main thread
notifyMainThread(NoObjectsFetched, payload: nil)
return
}
}
if let objcts = JSON["SomeObjects"] as? NSArray {
if let SomeObjects = parseResultingObjects(objcts) {
//- Return notification to be handled in main thread
notifyMainThread(ObjectsFetched, payload: ["payload": SomeObjects])
}
return
}
}
}
catch {
print("Error getting resulting objects")
}
}
else if let e = error {
print("\(e), could not process GET request")
}
}
I've also tried parsing the resulting objects on the main thread but
that doesn't seem to make a difference.
If you are curious, this is how I'm sending data to the main thread:
private func notifyMainThread(notification: String, payload: AnyObject?) {
dispatch_async(dispatch_get_main_queue(), {
if let p = payload {
NSNotificationCenter.defaultCenter().postNotificationName(notification, object: nil,
userInfo: p as! [String: [MYMODEL]])
}
else {
NSNotificationCenter.defaultCenter().postNotificationName(notification, object: nil)
}
});
}
What I've found out:
Nothing makes sense! I've attempted debugging this but I cant really pin point what the issue is, When the debugger hits my "getAllObjects" method, it can take a good few seconds (up to 45 seconds) before the server logs that it received and processed the request (which it usually takes around 30 milliseconds). As far as I can tell, this happens for all requests types. Also, once the application gets the data back (super fast), it takes a long time (around 4 seconds) to parse it, and its only around 11kbs.
I've also attempted to change the requests cache policy in case the application was checking the validity of the cached records with the server, I used ReloadIgnoringLocalAndRemoteCachedData which didn't work either.
Now, this sounds like a memory leak
If I pause the application at any point (after using it for a few minutes), I can see a concerning number of threads. I'm honestly not too familiar with IOS so I'm unsure if this threads are from the simulator or if they all belong to the app, the app streams video contents (which has no latency issues) usin the AVPlayer class and I believe that many of this threads are related to this, however I'm unsure if this is normal, here's a screenshot of what I mean (Note the scroll bar T_T) Screenshot
Could it be that I've got a memory leak or some zombie threads considerably slowing the performance of my app? The only really noticeable delay happens only on HTTP requests which is quite odd, no other part of my UI lags, and no other feature in my app suffers from performance issues (even streaming video contents from a url).
What would be the best way to profile this performance issues to pin point the source of the problem?
UPDATE 1:
Thanks to the suggestions by Scriptable, I've managed to address the threading issue (caused by multiple AVPlayers doing their thing). The performance of the requests however are not solved.
Its worth pointing out, the server is physically in the same country as where I'm making the requests from, when making requests from the browser or form the Command Line the requests are almost immediate.
Also, when I randomly pause the app (while I wait to the request to happen) I can see a 'mach_msg_trap' in some of the threads, I'm not familiar with this but I believe this might be a race condition? or a deadlock?