Very slow post request with Swift's URLSession.shared.dataTask - swift

I'm currently porting an Android app over to iOS and I've noticed a significant decrease in performance for my HTTPS post requests. On Android, using Java HttpsURLConnection objects, post requests would take about 0.5-1.0 seconds on average (for transferring a maximum of about 100 characters). On Swift, using URLSession.shared.dataTask to perform the same post requests takes anywhere from half a second to 15 seconds, sometimes timing out.
One trend I've noticed is that, while running the app, the various requests that are made are either all slow (> 5 seconds) or all faster. The time for requests sporadically changes every time the app is restarted.
Below is my current dataTask code. getResponse and onSuccess are functions for response handling, but I've found that they aren't what's slowing down the requests. From a few "tests" (print statements), it seems like the slow-downs occur as network connections are first being established.
func execute() {
guard let url = URL(string: "https://website.com") else {
print("ERROR > INVALID URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postDataString = "data"
guard let requestBody: Data = postDataString.data(using: String.Encoding.utf8) else {
print("ERROR > FAILED TO ENCODE POST STRING")
return
}
request.httpBody = requestBody
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if let unwrappedError = error {
print("ERROR > FAILED TO CONNECT TO SERVER: \(error)")
return
}
guard let unwrappedData = data else {
print("ERROR > FAILED TO GET DATA")
return
}
guard let dataString = String(data: data, encoding: String.Encoding.utf8)?.replacingOccurrences(of: "+", with: "%2B") else {
print("ERROR > FAILED TO DECODE RESPONSE TO STRING")
return
}
let dataStringLines: [String] = dataString.components(separatedBy: "\n")
guard let unwrappedGetResponse = self.getResponse else {
print("ERROR > getResponse FUNCTION UNINITIALIZED")
return
}
let success: Bool = unwrappedGetResponse(dataStringLines)
if (success) {
guard let unwrappedOnSuccess = self.onSuccess else {
print("ERROR > onSuccess FUNCTION UNINITIALIZED")
return
}
DispatchQueue.main.async {
unwrappedOnSuccess()
}
}
else {
print("ERROR > BAD SERVER RESPONSE")
}
}
task.resume()
}
Some additional information: I'm testing the code with an iPhone 5S running iOS 12.4.5. The app is targeting iOS 10.0.
Any idea on what could cause such inconsistent performance?

Related

Swift, URLSession downloading certain data in playgrounds, but not in Xcode project

I am trying to do some parsing of HTML on client side using Swift inside Xcode Project. I first tested this function inside playgrounds for a variety of URLs, and it downloads instantly for all my use cases. However, running this inside my Xcode project for iOS (even when disabling ATS in my info.plist), the URLSession will not download anything for many of the URLs to common websites that worked in playgrounds. It will still download some, such as the html of apple.com. Can anybody explain what I might be missing or need to enable/disable to get this to work.
func fetchHTMLString(url: URL) {
let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
if let localURL = localURL {
if let string = try? String(contentsOf: localURL) {
print("String here")
self.sortData(htmlString: string)
} else {
print("couldnt get as string")
}
}
}
task.resume()
print("going")
}
Update, I attempted to change this function to use URLSession data task, and was able to successfully download. I am, however, now just curious to find out why this would allow the download to complete with data task and not with download task. Here's the code that works
func fetchHTMLString(url: URL) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error \(error.localizedDescription)")
} else if let data = data, let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
if let string = String(data: data, encoding: .utf8) {
print(string)
self.sortData(htmlString: string)
print("String here")
} else {
print("couldn't get as string")
}
} else {
print("Error \(response.statusCode)")
}
} else {
print("No data or error returned.")
}
}
task.resume()
print("going")
}
```

Making HTTP GET request with Swift 5

I am obviously missing something very fundamental/naïve/etc., but for the life of me I cannot figure out how to make simple GET requests.
I'm trying to make an HTTP GET request with Swift 5. I've looked at these posts/articles: one, two, but I can't get print() statements to show anything. When I use breakpoints to debug, the entire section within the URLSession.shared.dataTask section is skipped.
I am looking at the following code (from the first link, above):
func HTTP_Request() {
let url = URL(string: "http://www.stackoverflow.com")!
let task = URLSession.shared.dataTask(with: url) {(data: Data?, response: URLResponse?, error: Error?) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
}
HTTP_Request()
I am running this in a MacOS Command Line Project created through XCode.
I would greatly appreciate any help I can get on this, thank you.
Right now, if there is an error, you are going to silently fail. So add some error logging, e.g.,
func httpRequest() {
let url = URL(string: "https://www.stackoverflow.com")! // note, https, not http
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let string = String(data: data, encoding: .utf8)
else {
print(error ?? "Unknown error")
return
}
print(string)
}
task.resume()
}
That should at least give you some indication of the problem.
A few other considerations:
If command line app, you have to recognize that the app may quit before this asynchronous network request finishes. One would generally start up a RunLoop, looping with run(mode:before:) until the network request finishes, as advised in the run documentation.
For example, you might give that routine a completion handler that will be called on the main thread when it is done. Then you can use that:
func httpRequest(completion: #escaping () -> Void) {
let url = URL(string: "https://www.stackoverflow.com")! // note, https, not http
let task = URLSession.shared.dataTask(with: url) { data, response, error in
defer {
DispatchQueue.main.async {
completion()
}
}
guard
error == nil,
let data = data,
let string = String(data: data, encoding: .utf8)
else {
print(error ?? "Unknown error")
return
}
print(string)
}
task.resume()
}
var finished = false
httpRequest {
finished = true
}
while !finished {
RunLoop.current.run(mode: .default, before: .distantFuture)
}
In standard macOS apps, you have to enable outgoing (client) connections in the “App Sandbox” capabilities.
If playground, you have to set needsIndefiniteExecution.
By default, macOS and iOS apps disable http requests unless you enable "Allow Arbitrary Loads” in your Info.plist. That is not applicable to command line apps, but you should be aware of that should you try to do this in standard macOS/iOS apps.
In this case, you should just use https and avoid that consideration altogether.
Make sure the response get print before exiting the process, you could try to append
RunLoop.main.run()
or
sleep(UINT32_MAX)
in the end to make sure the main thread won't exit. If you want to print the response and exit the process immediately, suggest using DispatchSemaphore:
let semphare = DispatchSemaphore(value: 0)
func HTTP_Request() {
let url = URL(string: "http://www.stackoverflow.com")!
let task = URLSession.shared.dataTask(with: url) {(data: Data?, response: URLResponse?, error: Error?) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
semphare.signal()
}
task.resume()
}
HTTP_Request()
_ = semphare.wait(timeout: .distantFuture)
This works for me many times I suggest you snippet for future uses!
let url = URL(string: "https://google.com")
let task = URLSession.shared.dataTask(with: ((url ?? URL(string: "https://google.com"))!)) { [self] (data, response, error) in
do {
let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: [])
print(jsonResponse)
guard let newValue = jsonResponse as? [String:Any] else {
print("invalid format")
}
}
catch let error {
print("Error: \(error)")
}
task.resume()
}

Alamofire causes crash on iPhone 5

I currently give some users access to my app via TestFlight so that they can test it. On every iPhone the App works perfectly without any problems but on the iPhone 5 the app crashes every time. In the xCode simulator everything works properly but not on a real device.
It looks like Alamofire causes the crash. The problem is in Alamofire>Source>Features>ResponseSerialization.swift>response(queue:completionHandler:)
Here is the code from the function:
/// Adds a handler to be called once the request has finished.
///
/// - parameter queue: The queue on which the completion handler is dispatched.
/// - parameter completionHandler: The code to be executed once the request has finished.
///
/// - returns: The request.
#discardableResult
public func response(queue: DispatchQueue? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Self {
delegate.queue.addOperation {
(queue ?? DispatchQueue.main).async {
var dataResponse = DefaultDataResponse(
request: self.request,
response: self.response,
data: self.delegate.data,
error: self.delegate.error
)
dataResponse.add(self.delegate.metrics)
completionHandler(dataResponse)
}
}
return self
}
completionHandler(dataResponse) looks like it is the problem.
Below there is also a screenshot from xCode
Is this a Alamofire related problem? Because on every other device (5s, 6 Plus, 7 Plus, SE and 7) it works without any problems. The crash occur when the iPhone is connected to WiFi and when it uses the mobile network.
Thanks for any tips!
EDIT:
This should be the code section that is called when starting the application:
Alamofire.request("https://app.site.tld/mobile/ios", parameters: parameters).response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.data)")
if response.response != nil{
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
let weatherDataArr = utf8Text.components(separatedBy: "~")
guard let hash = String(weatherDataArr[0]) else {
completion(nil)
return
}
do {
//working with received data.
//let currData: wData = try wData(hash: hash, ....)
completion(currData)
} catch {
print("error creating Object: \(error)")
completion(nil)
}
}
else {
completion(nil)
}
}else {
completion(nil)
}
}
Just found a warning in xCode that says:
'catch' block is unreachable because no errors are thrown in 'do' block
First simplify to remove clutter
Alamofire.request("https://app.site.tld/mobile/ios", parameters: parameters).response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.data)")
var result: wData? = nil
defer { completion(result) }
guard response.response != nil else { return }
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
let weatherDataArr = utf8Text.components(separatedBy: "~")
guard let hash = String(weatherDataArr[0]) else { return }
//working with received data.
//let currData: wData = try wData(hash: hash, ....)
result = currData
}
}
Then we can ask ourselves
How are you 100% sure there are elements in weatherDataArr? If not, use guard let hash = weatherDataArr.first else { return }
What happens under //working with received data.?

Making multiple asynchronous HTTP requests in succession and writing with Realm

I'm currently using Alamofire for requesting data and writing to disk with Realm. Specifically, I am fetching 24 source URLS from a Facebook Graph GET request and then making 24 separate requests to retrieve the data for each image. Once the data is retrieved, I am writing to disk with Realm.
here is how I am fetching the 24 sources:
FBAPI
Alamofire.request(.GET, FBPath.photos, parameters: params).responseJSON { response in
guard response.result.error == nil else {
print("error calling GET on \(FBPath.photos)")
print(response.result.error!)
completion(latestDate: nil, photosCount: 0, error: response.result.error)
return
}
if let value = response.result.value {
let json = JSON(value)
if let photos = json[FBResult.data].array {
for result in photos {
let manager = PTWPhotoManager()
manager.downloadAndSaveJsonData(result)
}
As you can see, I have a for loop iterating through each JSON containing the source url for the photo's image in which I then make another network request for each url, like so:
Manager
func downloadAndSaveJsonData(photoJSON : JSON) {
let source = photoJSON[FBResult.source].string
let id = photoJSON[FBResult.id].string
let created_time = photoJSON[FBResult.date.createdTime].string
let imageURL = NSURL(string: source!)
print("image requested")
Alamofire.request(.GET, imageURL!).response() {
(request, response, data, error) in
if (error != nil) {
print(error?.localizedDescription)
}
else {
print("image response")
let photo = PTWPhoto()
photo.id = id
photo.sourceURL = source
photo.imageData = data
photo.createdTime = photo.createdTimeAsDate(created_time!)
let realm = try! Realm()
try! realm.write {
realm.add(photo)
}
print("photo saved")
}
}
}
There seems to be a very long delay between when each image's data is requested and when I receive a response, and it also does not appear to be asynchronous. Is this a threading issue or is there a more efficient way to request an array of data like this? It should also be noted that I am making this network request from the Apple Watch itself.
These requests will happen mostly asynchronous as you wish. But there is some synchronization happening, you might been not aware of:
The response closures for Alamofire are dispatched to the main thread. So your network responses competes against any UI updates you do.
Realm write transactions are synchronous and exclusive, which is enforced via locks which will block the thread where they are executed on.
In combination this both means that you will block the main thread as long as the network requests succeed and keep coming, which would also render your app unresponsive.
I'd recommend a different attempt. You can use GCD's dispatch groups to synchronize different asynchronous tasks.
In the example below, the objects are all kept in memory until they are all downloaded.
A further improvement could it be to write the downloaded data onto disk instead and store just the path to the file in the Realm object. (There are plenty of image caching libraries, which can easily assist you with that.)
If you choose a path, which depends only on the fields of PWTPhoto (or properties of the data, you can get through a quick HEAD request), then you can check first whether this path exists already locally before downloading the file again. By doing that you save traffic when updating the photos or when not all photos could been successfully downloaded on the first attempt. (e.g. app is force-closed by the user, crashed, device is shutdown)
class PTWPhotoManager {
static func downloadAllPhotos(params: [String : AnyObject], completion: (latestDate: NSDate?, photosCount: NSUInteger, error: NSError?)) {
Alamofire.request(.GET, FBPath.photos, parameters: params).responseJSON { response in
guard response.result.error == nil else {
print("error calling GET on \(FBPath.photos)")
print(response.result.error!)
completion(latestDate: nil, photosCount: 0, error: response.result.error)
return
}
if let value = response.result.value {
let json = JSON(value)
if let photos = json[FBResult.data].array {
let group = dispatch_group_create()
var persistablePhotos = [PTWPhoto](capacity: photos.count)
let manager = PTWPhotoManager()
for result in photos {
dispatch_group_enter(group)
let request = manager.downloadAndSaveJsonData(result) { photo, error in
if let photo = photo {
persistablePhotos.add(photo)
dispatch_group_leave(group)
} else {
completion(latestDate: nil, photosCount: 0, error: error!)
}
}
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
let realm = try! Realm()
try! realm.write {
realm.add(persistablePhotos)
}
let latestDate = …
completion(latestDate: latestDate, photosCount: persistablePhotos.count, error: nil)
}
}
}
}
}
func downloadAndSaveJsonData(photoJSON: JSON, completion: (PTWPhoto?, NSError?) -> ()) -> Alamofire.Request {
let source = photoJSON[FBResult.source].string
let id = photoJSON[FBResult.id].string
let created_time = photoJSON[FBResult.date.createdTime].string
let imageURL = NSURL(string: source!)
print("image requested")
Alamofire.request(.GET, imageURL!).response() { (request, response, data, error) in
if let error = error {
print(error.localizedDescription)
completion(nil, error)
} else {
print("image response")
let photo = PTWPhoto()
photo.id = id
photo.sourceURL = source
photo.imageData = data
photo.createdTime = photo.createdTimeAsDate(created_time!)
completion(photo, nil)
}
}
}
}

NSURLRequest with cachePolicy ReloadIgnoringCacheData return stale data

I have an usecase where i need to poll a rest api for authentication on Apple TV. I get stale response JSON response for 90-120 sec after which i am getting proper JSON response.
Below is my code
static func getFileNoCache(url:NSURL?, completionHandler:(NSData?, String?)->Void) {
if let fileUrl = url {
let request = NSURLRequest(URL: fileUrl, cachePolicy: .ReloadIgnoringCacheData, timeoutInterval: 5)
let dataTask = NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: { data, response, error in
if let err = error {
// failed !
print("!! Error - Download Failed \n\t\(fileUrl) reason:\(err.localizedDescription)")
completionHandler (nil, err.localizedDescription)
return
}
if let statusCode = (response as? NSHTTPURLResponse)?.statusCode {
if statusCode == 200 {
completionHandler(data, nil)
}
else {
completionHandler (nil, "Message")
}
}
else {
completionHandler (nil, "Invalid response")
print("!! Error - Downloading EPG Config")
}
})
dataTask.resume()
}
}
I m really clueless whats going wrong
Could be any number of things, from proxies to server-side caching to bugs in the shared session's behavior.
Try disabling the cache entirely by using your own session configuration, setting its URLCache property to nil, then creating a session based on that configuration.
If that doesn't work, then the caching is not in your machine, and your best bet is to add a cache-buster to the URL (e.g. &ignoredParameter=monotonically_increasing_number).