I'm currently implementing an App which has a collectionView. The cells in the collection view are filled via an API-request. The requests are to the same API but with different locations. So for example for the first cell I'm calling: "google.de/GamesInItaly" and for the second cell: "google/GamesInGermany". While I'm scrolling down the collectionView the appearing cells should also make an API-request that has different countries.
The Problem I'm facing is, that I don't know how to handle the different API calls.
I managed to make one API-request and fill every cell with the same data, but I want different data for every cell.
My code looks like this:
CollectionView:
Call with from collectionView:
func fetchMatch() {
ApiService.sharedInstance.fetchGames(from: contentURL, completion: { (content: [[contentModel]]) in
self.content = content
self.collectionView.reloadData()
})
}
API-Services:
Set the Url:
func fetchGames(from url: String, completion: #escaping ([contentModel]) -> ()) {
fetchFeedForUrlString(urlString: "\(BaseUrl)/getgames/\(url)", completion: completion)
}
Fetch function:
func fetchFeedForUrlString<T: Decodable>(urlString: String, completion: #escaping (T) -> ())
{
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
guard let data = data else {
return
}
do {
let json = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(json)
}
} catch let jsonError {
print(jsonError)
}
}
task.resume()
}
I think what I could do is make many of these fetchGames functions, but I think that would be a bad practice.
I've also read about DispatchGroup but I'm not really sure how I can implement these into my program.
Does somebody know how I can handle multiple API-calls in a good practice?
Related
I am working on this app that helps me run some NLP on tweets & display results in a feed using a TableView.
Up to today, my app was running all the NLP on-device with a custom model built with CreateML & Apple's NaturalLanguage framework. When I would open the app, the tweets would be analyzed & show the results in the feed.
To increase the accuracy of results, I set up my own API & now make a call to that API to do some extra analysis. The issue now is that when I open the app, there is some kind of race condition. The feed does not show anything until I refresh. In the console, I see that the feed is done running fetchAndAnalyze() that gets the results while the API call in tripleCheckSentiment() is not completed.
Here is some explanation around the architecture.
NetworkingAPI (only the relevant code):
// This function makes a call to the Twitter API & returns a JSON of a user's tweets.
static func getUserTimeline(screenName: String, completion: #escaping (JSON) -> Void) {
client.sendTwitterRequest(request) { (response, data, connectionError) -> Void in
if connectionError != nil {
print("Error: \(connectionError)")
}
do {
let json = try JSON(data: data!)
completion(json)
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
}
}
}
// This function makes a call to my API & checks the sentiment of a Tweet.
static func checkNegativeSentiment(tweet: Tweet, completion: #escaping (JSON) -> Void) {
let headers: HTTPHeaders = ["Content-Type": "application/json"]
AF.request(apiURL, method: .post, parameters: tweet, encoder: JSONParameterEncoder.default, headers: headers).response {
response in
do {
let json = try JSON(data: response.data!)
completion(json)
} catch let jsonError as NSError {
print("json error: \(jsonError.localizedDescription)")
completion(JSON.init(parseJSON: "API OFFLINE."))
}
}
}
TweetManager (only the relevant code):
// This function is called from the app's feed to retrieve the most recent tweets.
func fetchTweets(completion: #escaping (Bool) -> Void) {
for friend in Common.listOfFriends {
NetworkingAPI.getUserTimeline(screenName: friend.handle, completion: {
success in
self.parseTweets() // This puts all the tweets returned in success in a list.
self.analyze() // Runs some NLP on the tweets.
completion(true)
})
}
}
func analyze() {
for tweet in listOfTweets {
// Does some on-device NLP using a model created with CreateML ...
if sentimentScore == "0" { // That is the tweet is negative.
doubleCheckSentiment(tweet: tweet)
}
}
}
func doubleCheckSentiment(tweet: Tweet) {
// Does some on-device NLP using Apple's generic framework NaturalLanguage.
if sentimentScore <= -0.8 { // Once again, the tweet is negative.
tripleCheckSentiment(tweet: tweet)
}
}
func tripleCheckSentiment(tweet: Tweet) {
NetworkingAPI.checkNegativeAzureSentiment(tweet: tweet, completion: {
json in
if json["value"]["sentiment"].int! == 2 { // We confirm the tweet is negative.
Common.negativeTweets.append(tweet)
}
}
}
FeedVC (only the relevant code):
// This function gets called when the view appears & at a bunch of different occasions.
func fetchAndAnalyze() {
var friendsAnalyzed = 0
tweetManager.fetchTweets(completion: {
success in
friendsAnalyzed += 1 // Every time completion hits, it means one friend was analyzed.
if friendsAnalyzed == Common.listOfFriends.count { // Done analyzing all friends.
self.tableView.reloadData() // Refresh & show the tweets in Common.negativeTweets in table.
}
}
I know this is long & I deeply apologize but if I could get some help on this, I would really appreciate it! By the way, excuse my use of #escaping & all that, I am fairly new to handling asynchronous API calls.
Thanks!
**EDIT, after implementing jawadAli's solution which works in some cases for some reason, I notice the following pattern: **
Imagine I add a friend to my listOfFriends. Then I refresh, which calls fetchAndAnalyze(). We see in the log REFRESH CALLED. & by the end of the function call that no negative tweets were found. Right after this happened, we get a result from our API call that one tweet was found negative.
If I refresh again, then that tweet is displayed. Any clue?
There is an issue with this function. On first transection of for loop your completion get fired ..
func fetchTweets(completion: #escaping (Bool) -> Void) {
let myGroup = DispatchGroup()
for friend in Common.listOfFriends {
myGroup.enter()
NetworkingAPI.getUserTimeline(screenName: friend.handle, completion: {
success in
self.parseTweets()
self.analyze()
myGroup.leave()
})
}
}
myGroup.notify(queue: DispatchQueue.main) {
completion(true)
})
Also reload data on Main thread
DispatchQueue.main.async {
self.tableView.reloadData()
}
NOTE: You need to handle success and failure case accordingly.. i am just giving an idea how to use dispatchGroup to sync calls ...
Background
The function below calls two functions, which both access an API, retrieve JSON data, parse through it, etc, and then take that data and populates the values of an object variable in my View Controller class.
func requestWordFromOxfordAPI(word: String, completion: (_ success: Bool) -> Void) {
oxfordAPIManager.fetchDictData(word: word)
oxfordAPIManager.fetchThesData(word: word)
completion(true)
}
Normally, if there was only one function fetching data, and I wanted to call a new function that takes in the data results and does something with them, I would use a delegate method and call it within the closure of the data fetching function.
For Example:
Here, I fetch data from my firebase database and if retrieving the data is succesful, I call self.delegate?.populateWordDataFromFB(result: combinedModel). Since closures occur on separate thread, this ensures that the populateWordDataFromFB function runs only once retrieving the data has finished. Please correct me if I am wrong. I have just recently learned this and am still trying to see the whole picture.
func readData(word: String) {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
self.delegate?.populateWordDataFromFB(result: combinedModel)
} else {
self.delegate?.fbDidFailWithError(error: nil, summary: "\(word) not found, requesting from OxfordAPI")
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
}
case .failure(let error):
self.delegate?.fbDidFailWithError(error: error, summary: "Error decoding CombinedModel")
}
}
}
Also notice from the above code that if the data is not in firebase, I call the delegate method below, which is where I am running into my issue.
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
My Issue
What I am struggling with is the fact that the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions both have closures.
The body of these functions look like this:
if let url = URL(string: urlString) {
var request = URLRequest(url: url)
request.addValue(K.APISettings.acceptField, forHTTPHeaderField: "Accept")
request.addValue(K.APISettings.paidAppID , forHTTPHeaderField: "app_id")
request.addValue(K.APISettings.paidAppKey, forHTTPHeaderField: "app_key")
let session = URLSession.shared
_ = session.dataTask(with:request) { (data, response, error) in
if error != nil {
self.delegate?.apiDidFailWithError(error: error, summary: "Error performing task:")
return
}
if let safeData = data {
if let thesaurusModel = self.parseThesJSON(safeData) {
self.delegate?.populateThesData(thesModel: thesaurusModel, word: word)
}
}
}
.resume()
} else {print("Error creating thesaurus request")}
I assume both of these functions are running on separate threads in the background. My goal is to call another function once both the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions run. These two functions will populate the values of an object variable in my view controller which I will use in the new function. I don't want the new function to be called before the object variable in the view controller is populated with the right data so I tried to implement a completion handler. The completion handler function is being called BEFORE the two functions terminate, so when the new function tries to access the object variable in the View Controller, it's empty.
This is my first time trying to implement a completion handler and I tried to follow some other stack overflow posts but was unsuccessful. Also if this is the wrong approach let me know too, please. Sorry for the long explanation and thank you for any input.
Use DispatchGroup for this,
Example:
Create a DispatchGroup,
let group = DispatchGroup()
Modify the requestWordFromOxfordAPI(word: completion:) method to,
func requestWordFromOxfordAPI(word: String, completion: #escaping (_ success: Bool) -> Void) {
fetchDictData(word: "")
fetchThesData(word: "")
group.notify(queue: .main) {
//code after both methods are executed
print("Both methods executed")
completion(true)
}
}
Call enter() and leave() methods of DispatchGroup at the relevant places in fetchDictData(word:) and fetchThesData(word:) methods.
func fetchDictData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
func fetchThesData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
At last call requestWordFromOxfordAPI(word: completion:)
requestWordFromOxfordAPI(word: "") { (success) in
print(success)
}
I have an app that downloads some information from a URL (ie. author name, story title, and the cover image). I'm able to download and parse the JSON from the server properly, but I'm stuck at one point.
The app consists of one View Controller (called ViewController.swift) and one class file (called GetStories.swift). Once the app has finished download and parsing the JSON from the server, I want the table view in the View Controller to reload itself (self.tableView.reloadData()).
I've set up a chain of completion blocks in GetStories.swift that accomplish the following steps in order:
1) Download the JSON
2) Parse the JSON
3) Save it to disk
func updateUI(){
saveDownloadedAndParsedJSONToDisk {
}
}
func saveDownloadedAndParsedJSONToDisk(completionHandler: #escaping RefreshTableView){
parseJSON {
self.saveDataToJSON()
completionHandler()
}
}
func parseJSON(completionHandler: #escaping ReadyToSave){
downloadJSON { jsonPayload, error in
do {
if let data = jsonPayload {
self.stories = try JSONDecoder().decode(Stories.self, from: data)
if let stories = self.stories {
self.stories = stories
completionHandler()
} else {
print("An error occurred while decoding JSON.")
}
} else if let error = error {
print("Error retrieving data: \(error)")
}
} catch {
print(error.localizedDescription)
}
}
}
func downloadJSON(completionHandler: #escaping NetworkResponse){
let storiesAPIURL = URL(string: "\(wattpadAPIURL)")
var wattpadAPIRequest = URLRequest(url: storiesAPIURL!)
wattpadAPIRequest.httpMethod = "GET"
let session = URLSession.shared
let dataTask = session.dataTask(with: wattpadAPIRequest) { (data : Data?, response : URLResponse?, error : Error?) in
if let data = data {
completionHandler(data, nil)
} else if let error = error {
completionHandler(nil, error)
print(error.localizedDescription)
}
}
dataTask.resume()
}
In ViewController.swift, I am calling updateUI(). Then, I'm calling the delegate method in the protocol:
self.storyResults?.delegate?.didFinishFetchingAndParsingData(finished: true)
The delegate method is doing this:
func didFinishFetchingAndParsingData(finished: Bool) {
guard finished else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
}
So, as you see, I'm using a 0.5 second delay to reload the table view because I'm not sure how to tell when exactly the downloading, parsing and saving has all fully finished.
If I don't use the delay of 0.5 seconds in the delegate method, the table view gets reloaded at an inappropriate time and there are no results displayed as a result. So, executing the reload this way doesn't work:
DispatchQueue.main.async {
self.tableView.reloadData()
}
What is the proper way to do this?
Thanks in advance!
Move the delegate call to the completionHandler inside updateUI, then it will be called at the right moment. Right now you have an empty completionHandler there.
func updateUI() {
saveDownloadedAndParsedJSONToDisk {
DispatchQueue.main.async {
self.storyResults?.delegate?.didFinishFetchingAndParsingData(finished: true)
}
}
}
...
func didFinishFetchingAndParsingData(finished: Bool) {
guard finished else {
return
}
self.tableView.reloadData()
SVProgressHUD.dismiss()
}
I have a UITable-View with a List of Users and their Profile Pictures.
I am loading the pictures (http://api/pictures/{userid}) one by one for each player asynchronous:
func loadImageAsync(imageUrl: URL, completionHandler handler: #escaping (_ success: Bool, _ image: UIImage?) -> Void){
DispatchQueue.global(qos: .userInitiated).async { () -> Void in
if let imgData = try? Data(contentsOf: imageUrl), let img = UIImage(data: imgData) {
DispatchQueue.main.async(execute: { () -> Void in
handler(true, img)
})
} else {
handler(false, nil)
}
}
In the completion handler in the cellForRowAt-Index-Fuction, I am setting the pictures.
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
if(success){
cell.profilePictureView.image = image
}
}
However, when I scroll very fast, some pictures get loaded in the wrong cells.
To prevent reuse-issues, I am "resetting" the image view after every reuse:
override func prepareForReuse() {
profilePictureView.image = UIImage(named: "defaultProfilePicture")
}
But why are still some images loaded false when scrolling fastly?
hmmm, this is what I thought too.
__Update:
So, I extended the function with a Label Parameter (type Any), that is returned back as it was put in the function. I tried to compare the parameter (is used the indexpath) with the current indexpath. Actually, this should work - shouldn't it?!
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
cell.loader.stopAnimating()
if (backlabel as! IndexPath == indexPath) {
//set image...
But however, it doesn't show any effect. Do you know why or have any other solutions to fix this?
The issue is that if you scroll fast, the download may take long enough that by the time it's complete, the cell in question has scrolled off the screen and been recycled for a different indexPath in your data model.
The trick is to ask the table view for the cell at that indexPath in the completion block and only install the image if you get a cell back:
loadImageAsync(imageUrl: imageUrl!, label: ip, for indexPath: IndexPath) { (success, image, backlabel) -> Void in
if(success){
let targetCell = tableview.cell(for: indexPath)
targetCell.profilePictureView.image = image
}
}
EDIT:
Redefine your loadImageAsync function like this:
func loadImageAsync(imageUrl: URL,
indexPath: IndexPath,
completionHandler handler: #escaping (_ success: Bool,
_ image: UIImage?,
_ indexPath: IndexPath ) -> Void) { ... }
EDIT #2
And by the way, you should really save your images to disk and load them from there rather than loading from the internet each time. I suggest using a hash of the image URL as a filename.
Modify loadImageAsync as follows:
Check to see if the file already exists on disk. If so, load it and return it.
If the file does not exist, do the async load, and then save it to disk using the hash of the URL as a filename, before returning the in-memory image.
Because your completionHandler can be called after the cell has been reused for the next user, and possibly another image request for the cell has been fired. The order of events (reuse/completion) is not predictable, and in fact a later async request could complete before an earlier one.
I have a header view for every UITableViewCell. In this header view, I load a picture of an individual via an asynchronous function in the Facebook API. However, because the function is asynchronous, I believe the function is called multiple times over and over again, causing the image to flicker constantly. I would imagine a fix to this issue would be to load the images in viewDidLoad in an array first, then display the array contents in the header view of the UITableViewCell. However, I am having trouble implementing this because of the asynchronous nature of the function: I can't seem to grab every photo, and then continue on with my program. Here is my attempt:
//Function to get a user's profile picture
func getProfilePicture(completion: (result: Bool, image: UIImage?) -> Void){
// Get user profile pic
let url = NSURL(string: "https://graph.facebook.com/1234567890/picture?type=large")
let urlRequest = NSURLRequest(URL: url!)
//Asynchronous request to display image
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue()) { (response:NSURLResponse!, data:NSData!, error:NSError!) -> Void in
if error != nil{
println("Error: \(error)")
}
// Display the image
let image = UIImage(data: data)
if(image != nil){
completion(result: true, image: image)
}
}
}
override func viewDidLoad() {
self.getProfilePicture { (result, image) -> Void in
if(result == true){
println("Loading Photo")
self.creatorImages.append(image!)
}
else{
println("False")
}
}
}
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
headerView.headerImage.image = self.creatorImages[section]
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
As seen by the program above, I the global array that I created called self.creatorImages which holds the array of images I grab from the Facebook API is always empty and I need to "wait" for all the pictures to populate the array before actually using it. I'm not sure how to accomplish this because I did try a completion handler in my getProfilePicture function but that didn't seem to help and that is one way I have learned to deal with asynchronous functions. Any other ideas? Thanks!
I had the same problem but mine was in Objective-C
Well, the structure is not that different, what i did was adding condition with:
headerView.headerImage.image
Here's an improved solution that i think suits your implementation..
since you placed self.getProfilePicture inside viewDidLoad it will only be called once section==0 will only contain an image,
the code below will request for addition image if self.creatorImages's index is out of range/bounds
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
//Show section header cell with image
var cellIdentifier = "SectionHeaderCell"
var headerView = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! SectionHeaderCell
if (section < self.creatorImages.count) // validate self.creatorImages index to prevent 'Array index out of range' error
{
if (headerView.headerImage.image == nil) // prevents the blinks
{
headerView.headerImage.image = self.creatorImages[section];
}
}
else // requests for additional image at section
{
// this will be called more than expected because of tableView.reloadData()
println("Loading Photo")
self.getProfilePicture { (result, image) -> Void in
if(result == true) {
//simply appending will do the work but i suggest something like:
if (self.creatorImages.count <= section)
{
self.creatorImages.append(image!)
tableView.reloadData()
println("self.creatorImages.count \(self.creatorImages.count)")
}
//that will prevent appending excessively to data source
}
else{
println("Error loading image")
}
}
}
headerView.headerImage.clipsToBounds = true
headerView.headerImage.layer.cornerRadius = headerView.headerImage.frame.size.width / 2
return headerView
}
You sure have different implementation from what i have in mind, but codes in edit history is not in vain, right?.. hahahaha.. ;)
Hope i've helped you.. Cheers!