Making multiple asynchronous HTTP requests in succession and writing with Realm - swift

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

Related

How to check if one of URLSession tasks returned an error and if so to stop code execution?

I need to make 2 API calls simultaneously. I have 2 URLs for the calls, and if one of the calls will return any error I want to stop all the code execution.
How I tried to do it:
I have a function called performRequest() with a completion block. I call the function in my ViewController to update the UI - show an error/or a new data if all was successful. Inside it I create a URLSession tasks and then parse JSON:
I created an array with 2 urls:
func performRequest(_ completion: #escaping (Int?) -> Void) {
var urlArray = [URL]()
guard let urlOne = URL(string: "https://api.exchangerate.host/latest?base=EUR&places=9&v=1") else { return }
guard let urlTwo = URL(string: "https://api.exchangerate.host/2022-05-21?base=EUR&places=9") else { return }
urlArray.append(urlOne)
urlArray.append(urlTwo)
}
Then for each of the url inside the array I create a session and a task:
urlArray.forEach { url in
let session = URLSession(configuration: .ephemeral)
let task = session.dataTask(with: url) { data, _, error in
if error != nil {
guard let error = error as NSError? else { return }
completion(error.code)
return
}
if let data = data {
let printData = String(data: data, encoding: String.Encoding.utf8)
print(printData!)
DispatchQueue.main.async {
self.parseJSON(with: data)
}
}
}
task.resume()
}
print("all completed")
completion(nil)
}
For now I receive print("all completed") printed once in any situation: if both tasks were ok, if one of them was ok or none of them.
What I want is to show the print statement only if all tasks were completed successfully and to stop executing the code if one of them returned with error (for example if we will just delete one of the symbols in url string which will take it impossible to receive a data).
How can I do it correctly?

Why my DateTask code block does not work?

I create a request to the server, and in the end I expect to receive data, which I then transform into a model using a function, for this I created a session
func fetchNewsData(forCoutry country: String, category: String, complition: #escaping (NewsDataModel) -> ()) {
let urlString = "some url string"
guard let url = URL(string: urlString) else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
}
task.resume()
}
but the following code just doesn't work
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
I used breakpoints to find out until what point everything is going well, and I realized that this particular block of code is not working.
when I set a breakpoint between the let session and the let task, the code stopped there, but when I set my code to an print(error), this breakpoint did not work
I used the function fetchNewsData in viewDidLoad and I want to work to fill the array with elements that I expect to receive from the data that will come on this request, but my array does not receive any elements, and it remains empty, because of this my application does not work
why part of the code doesn't work, and how can I get the data I need from it?
The problem turned out to be a poor understanding of closures
I was not calling my method correctly to get the data. Having figured it out, I realized that the problem is precisely in a different approach when calling this method

swift wait until the urlsession finished

each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images.
But what i can't do is reloading the collection view once the loop is finish.
func loadImages() {
DispatchQueue.main.async {
for _ in 1...11{
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
}
self.collectionV.reloadData()
print("after resume")
}
Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.
/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue
func loadImages(completion: #escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11
for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data else { return }
do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}
Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:
struct FoodImage: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url = "image"
}
}
Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,
var imageURLs: [URL]?
override func viewDidLoad() {
super.viewDidLoad()
// caller will update model and UI
loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}
Note:
The DispatchQueue.main.async is not necessary. These requests already run asynchronously.
Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)
The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.
Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.
When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.
In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.
Anyway, that yields:
If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:
private let group = DispatchGroup()
Then modify your loadImages() function:
func loadImages() {
for _ in 1...11 {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.group.leave()
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) { [weak self] in
self?.collectionV.reloadData()
}
}
Some description:
On the method call group.enter() will be called 11 times
On each completion of image downloading group.leave() will be called
When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()
More about DispatchGroup
Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.

Download multiple files containing data points one file per time

This code works perfectly when downloading one file. However when trying to download multiple files that contain data and caching the incoming data causes a mess. Since the download occurs non-stop, thus one file is done and the next starts. I can't cache them since I don't know which data belongs to which file.
lazy var downloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.maxConcurrentOperationCount = 1000000
queue.name = "Files"
return queue
}()
func fetch(url: String, completionHandler: #escaping ([String:String]) -> (), completionHandlerQueue: OperationQueue?) {
let task = session.dataTask(with: URL(string: url)!, completionHandler: {
(data, response, error) in
guard let data = data, let type = String(data: data, encoding: String.Encoding.utf8), error == nil else {
print("Error with the data: \(error.debugDescription)")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
return
}
guard completionHandlerQueue != nil else {
return
}
completionHandlerQueue!.addOperation(BlockOperation(block: {
completionHandler(type)
}))
})
task.resume()
}
// Here is the links
let urls = [.....]
func start() {
for url in urls {
print("Start")
fetch(url: url, completionHandler: { type in
// The incoming data I don't know which belongs to which url.
}, completionHandlerQueue: downloadQueue)
}
print("End")
}
Since its async I will get
Start
End
Then the data will come. How can I overcome this?

Swift and Facebook SDK 4, Graph API

Trying to put Facebook integration into a trial app I'm working on and can't seem to find an efficient way to make a friends list with a small profile pic from fb beside them. everything works flawless but the lengthy wait times on the data fetch. Please help with anything that will speed up image fetch times. Currently it takes about 10 seconds to fetch.
func facebookProfilePicRequest(){
let graphConnection = FBSDKGraphRequestConnection()
for result in self.facebookFriends {
if let resultingId = result["id"] as? String{
let profilePicRequest = FBSDKGraphRequest(graphPath: "/\(resultingId)/picture?redirect=false", parameters: nil)
graphConnection.addRequest(profilePicRequest){
(connection:FBSDKGraphRequestConnection!, result:AnyObject!, error:NSError!) -> Void in
if(error != nil){
if error.code == 1009{
println("No Internet Connection, \(error.code))")
}
}else{
if let data: NSDictionary = result as? NSDictionary{
if let urlDictionary: NSDictionary = data["data"] as? NSDictionary{
if let urlString: NSString = urlDictionary["url"] as? NSString{
let url: NSURL = NSURL(string: urlString as String)!
var request1: NSURLRequest = NSURLRequest(URL: url)
let queue:NSOperationQueue = NSOperationQueue()
NSURLConnection.sendAsynchronousRequest(request1, queue: queue, completionHandler:{ (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
if error == nil{
self.facebookFriendsImages.append(data)
self.tableView.reloadData()
}
})
}
}
}
}
}
}
}
graphConnection.start()
}
Instead of fetching all the images by yourself, you could use Facebook provided FBSDKProfilePictureView. You only have to set the user profile ID, and it would load the picture for you.
This should improve things as it doesn't have to wait for all the images to be loaded before showing them, and it also load them asynchronously as you scroll the tableview.