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 ...
Related
I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that's what's happening in the Webservices' struct:
struct Webservices {
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view's displayed directly to the view model, but still without success.
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe URLSession.shared.dataTask executes synchronously. It actually executes asynchronously. Because of this, iOS won't wait until your server response is received to execute the rest of your code.
To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the Webservices struct
When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() on it, and it starts executing on a different thread asynchronously.
Because it executes asynchronously, iOS doesn't wait for it to return to continue executing the rest of your code. As soon as the resume() method returns, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser has returned long ago.
So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don't need that function to return anything - it can process the response and carry on the rest of the work.
So this method signature:
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
becomes this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
And the changes to the requestNewUser looks like this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
completion(decodedResponse)
}
}.resume()
}
2. View Model Changes
The requestNewUser method now doesn't return anything. So we need to accommodate that change in our the rest of the code. Let's convert our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.
So your createNewUserVM changes from this:
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
to this:
func createNewUserVM(_ callback: #escaping (_ response: String?) -> Void) {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
callback("failure")
return
}
callback(serverResponse.response)
}
}
3. Moving to the next view
Now that createNewUserVM is also asynchronous, we also need to change how we call it from our controller.
So that code changes from this:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
To this:
self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
guard let `self` = self else { return }
if serverResponse == "success" {
// move to the next view
// self.present something...
}
}
Conclusion
I hope the answer gives you an idea of why your code didn't work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.
This can be achieve using DispatchGroup and BlockOperation together like below:
func functionWillEscapeAfter(time: DispatchTime, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: time) {
completion(false) // change the value to reflect changes.
}
}
func createNewUserAfterGettingResponse() {
let group = DispatchGroup()
let firstOperation = BlockOperation()
firstOperation.addExecutionBlock {
group.enter()
print("Wait until async block returns")
functionWillEscapeAfter(time: .now() + 5) { isSuccess in
print("Returned value after specified seconds...")
if isSuccess {
group.leave()
// and firstoperation will be complete
} else {
firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
group.leave()
}
}
group.wait() //Waits until async closure returns something
} // first operation ends
let secondOperation = BlockOperation()
secondOperation.addExecutionBlock {
// Now before executing check if previous operation was cancelled we don't need to execute this operation.
if !firstOperation.isCancelled { // First operation was successful.
// move to next view
moveToNextView()
} else { // First operation was successful.
// do something else.
print("Don't move to next block")
}
}
// now second operation depends upon the first operation so add dependency
secondOperation.addDependency(firstOperation)
//run operation in queue
let operationQueue = OperationQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
}
func moveToNextView() {
// move view
print("Move to next block")
}
createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!
I recently encounter two data fetching (download) API that performs seemingly the same thing to me. I cannot see when should I use one over the other.
I can use URLSession.shared.dataTask
var tasks: [URLSessionDataTask] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
let task = URLSession.shared.dataTask(with: tuple.imageURL, completionHandler :
{ data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() { [weak self] in
self?.displayFlag(data: data, title: tuple.name)
}
})
tasks.append(task)
task.resume()
}
deinit {
tasks.forEach {
$0.cancel()
}
}
Or I can use URLSession.shared.dataTaskPublisher
var cancellables: [AnyCancellable] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
URLSession.shared.dataTaskPublisher(for: tuple.imageURL)
.sink(
receiveCompletion: {
completion in
switch completion {
case .finished:
break
case .failure( _):
return
}},
receiveValue: { data, _ in DispatchQueue.main.async { [weak self] in self?.displayFlag(data: data, title: tuple.name) } })
.store(in: &cancellables)
}
deinit {
cancellables.forEach {
$0.cancel()
}
}
I don't see their distinct differences, as both also can fetch, and both also provide us the ability to cancel the tasks easily. Can someone shed some light on their differences in terms of when to use which?
The first one is the classic. It has been present for quite some time now and most if not all developers are familiar with it.
The second is a wrapper around the first one and allows combining it with other publishers (e.g. Perform some request only when first two requests were performed). Combination of data tasks using the first approach would be far more difficult.
So in a gist: use first one for one-shot requests. Use second one when more logic is needed to combine/pass results with/to other publishers (not only from URLSession). This is, basically, the idea behind Combine framework - you can combine different ways of async mechanisms (datatasks utilising callbacks being one of them).
More info can be found in last year's WWDC video on introducing combine.
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?
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)
}
Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}