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!!!
Related
I am trying to create a service object in my Swift application to handle requests a bit easier. I've got most of it wired up, however I might be misunderstanding completion handlers.
I have this function that simply posts to a local API endpoint I have running.
func createList(name: String, completion: #escaping (Response) -> Void) {
let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]
AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
switch response.result {
case .failure(let err):
print(err)
case .success(let res):
completion(res)
}
}
}
All that needs to happen is I need to pass that name to the function which I do here
barback.createList(name: name) -> after a button is tapped
However, I am now getting this error.
Missing argument for parameter 'completion' in call
My goal here is to just return the Response object so I can access attributes on that to do certain things in the UI. I was not able to return res here because, from my understanding, this is an async request and it's actually returning from that competition handler? (could be butchering that). The way I saw others doing this was by adding a competition handler to the params and adding an escape to that.
My end goal here is to do something like...
if barback.createList(name: name).status = 200
(trigger some UI component)
else
(display error toast)
end
Is my function flawed in it's design? I've tried changing my competition handler to be
completion: (#escaping (Response) -> Void) = nil
but run into some other errors there. Any guidance here?
Completion handlers are similar to function return values, but not the same. For example, compare the following functions:
/// 1. With return value
func createList(name: String) -> Response { }
/// 2. With completion handler
func createList(name: String, completion: #escaping (Response) -> Void) { }
In the first function, you'd get the return value instantly.
let response = barback.createList(name: name)
if response.status = 200 {
/// trigger some UI component
}
However, if you try the same for the second function, you'll get the Missing argument for parameter 'completion' in call error. That's because, well, you defined a completion: argument label. However, you didn't supply it, as matt commented.
Think of completion handlers as "passing in" a chunk of code into the function, as a parameter. You need to supply that chunk of code. And from within that chunk of code, you can access your Response.
/// pass in chunk of code here
barback.createList(name: name, completion: { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
})
Note how you just say barback.createList, not let result = barback.createList. That's because in the second function, with the completion handler, doesn't have a return value (-> Response).
Swift also has a nice feature called trailing closure syntax, which lets you omit the argument label completion:.
barback.createList(name: name) { response in
/// access `response` from within block of code
if response.status = 200 {
/// trigger some UI component
}
}
You can also refer to response, the closure's first argument, by using $0 (which was what I did in my comment). But whether you use $0 or supply a custom name like response is up to you, sometimes $0 is just easier to type out.
barback.createList(name: name) {
/// access $0 (`response`) from within block of code
if $0.status = 200 {
/// trigger some UI component
}
}
Calling createList would look something more like this:
barback.createList(name: name) { response in
if response.status == 200 {
// OK
} else {
// Error
}
}
This fixes the issue because you are now running this completion closure - { response in ... } - where response is the value you pass in. In this case, you pass in res. See this post about using completion handlers.
If you did want an optional completion handler so you don't always need to include it, you could change the definition to the following (adding = { _ in }, meaning it defaults to an empty closure):
func createList(name: String, completion: #escaping (Response) -> Void = { _ in })
Another way is actually making the closure optional:
func createList(name: String, completion: ((Response) -> Void)? = nil)
And then inside the method you need ? when you call completion, since it's optional:
completion?(res)
Use the completion handler as mentioned in the comments and answers.
In addition you should include a completion when it fails, otherwise you
will never get out of that function.
I would restructure your code to cater for any errors that might happens, like this:
func createList(name: String, completion: #escaping (Response?, Error?) -> Void) {
let parameters = ["name": name, "user_id": session.auth.currentUser!.uid]
AF.request("\(url)/wishlist", method: .post, parameters: parameters, encoding: URLEncoding.default).responseDecodable(of: Response.self) { response in
switch response.result {
case .failure(let err):
print(err)
completion(nil, err)
case .success(let res):
completion(res, nil)
}
}
}
call it like this:
barback.createList(name: name) { (response, error) in
if error != nil {
} else {
}
}
If you do not put a completion(...) in your "case .failure" it will never get out of there.
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 wanted to fetch data from the server api.
The issues is that all networking frameworks are doing it Async.
So I have issues that return variable return empty Here is my code.
The view controller where I call the function
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url = "http://api.musixmatch.com/ws/1.1/track.lyrics.get?track_id=12693365&apikey=63ee7da5e2ee269067ecc42b25590922"
let musixrequest = MusicMatchRequest()
let endResults = musixrequest.gettingLyrics(url: url)
if !endResults.isEmpty{
print("The end results are \(endResults)")
}else{
print("No results found")
}
}
Here is my class where I am trying to fetch the data
public class MusicMatchRequest : NSObject{
public override init(){}
public func gettingLyrics(url : String) -> String {
var endResults = ""
DefaultProvider.request(Route(path:"\(url)")).responseJSON { (response:Response<Any>) in
switch response.result{
case .success(let json):
endResults = String(describing:json)
print(endResults)
case .failure(let error):
print("error: \(error)")
}
}
return endResults
}
}
When I am printing the endRsults from the task it is working It print the results but the var endResults return empty.
Idea how to transfer the data .
I have tried two frameworks
Alamofire
Nikka
In both frameworks it's acting the same .
Solution
I don't exactly know what happens under the hood, but as ANY network operation this also has to be asynchronous (meaning it will take a certain amount of time to fetch the data).
let endResults = musixrequest.gettingLyrics(url: url)
If it's synchronously done on the Main thread, it will block it so the user can't interact with the app, which is pretty bad. Given it's asynchronous in your code you read the value in the very next line immediately, here:
if !endResults.isEmpty {
print("The end results are \(endResults)")
} else {
print("No results found")
}
It's very unlikely that the network operation will finish in one line step time, so you won't have the data there.
What you should do is to pass a completion handler in this method:
public func gettingLyrics(url : String) -> String
and dispatch to main thread like this:
DispatchQueue.main.async {
// do you UI stuff here
}
Change you function to this:
public func gettingLyrics(url : String, completionHandler: (String) -> Void)
and call the completion handler in the success branch:
completionHandler(String(describing:json))
I am using Firebase FirAuth API and before the API return result, Disposables.create() has been returned and it's no longer clickable (I know this might due to no observer.onCompleted after the API was called. Is there a way to wait for it/ listen to the result?
public func login(_ email: String, _ password: String) -> Observable<APIResponseResult> {
let observable = Observable<APIResponseResult>.create { observer -> Disposable in
let completion : (FIRUser?, Error?) -> Void = { (user, error) in
if let error = error {
UserSession.default.clearSession()
observer.onError(APIResponseResult.Failure(error))
observer.on(.completed)
return
}
UserSession.default.user.value = user!
observer.onNext(APIResponseResult.Success)
observer.on(.completed)
return
}
DispatchQueue.main.async {
FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: completion)
}
return Disposables.create()
}
return observable
}
You are correct in your assumption that an onError / onCompletion event terminate the Observable Sequence. Meaning, the sequence won't emit any more events, in any case.
As a sidenote to that, You don't need to do .on(.completed) after .onError() , since onError already terminates the sequence.
the part where you write return Disposables.create() returns a Disposable object, so that observable can later be added to a DisposeBag that would handle deallocating the observable when the DisposeBag is deallocated, so it should return immediately, but it will not terminate your request.
To understand better what's happening, I would suggest adding .debug() statements around the part that uses your Observable, which will allow you to understand exactly which events are happening and will help you understand exactly what's wrong :)
I had the same issue some time ago, I wanted to display an Alert in onError if there was some error, but without disposing of the observable.
I solved it by catching the error and returning an enum with the cases .success(MyType) and .error(Error)
An example:
// ApiResponseResult.swift
enum ApiResponseResult {
case error(Error)
case success(FIRUser)
}
// ViewModel
func login(...) -> Observable<ApiResponseResult> {
let observable = Observable.create { ... }
return observable.catchError { error in
return Observable<ApiResponseResult>.just(.error(error))
}
}
// ViewController
viewModel
.login
.subscribe(onNext: { result in
switch result {
case .error(let error):
// Alert or whatever
break
case .success(let user):
// Hurray
break
}
})
.addDisposableTo(disposeBag)
Im making a very basic app which has a search field to get data that is passed to a tableview.
What I want to do is run an Async task to get the data and if the data is succesfully fetched go to the next view, during the loading the screen must not freeze thats why the async part is needed.
When the user pressed the searchbutton I run the following code to get data in my
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
method.
var valid = true
let searchValue = searchField.text
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://someapi.com/search?query=" + searchValue!)
let task = session.dataTaskWithURL(url!, completionHandler: {(data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
if let theData = data {
dispatch_async(dispatch_get_main_queue(), {
//for the example a print is enough, later this will be replaced with a json parser
print(NSString(data: theData, encoding: NSUTF8StringEncoding) as! String)
})
}
else
{
valid = false;
print("something went wrong");
}
})
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
task.resume()
return valid;
I removed some code that checks for connection/changes texts to show the app is loading data to make the code more readable.
This is the part where I have the problem, it comes after all the checks and im sure at this part I have connection etc.
What happens is it returns true (I can see this because the view is loaded), but also logs "Something went wrong" from the else statement.
I Understand this is because the return valid (at the last line) returns valid before valid is set to false.
How can I only return true (which changes the view) if the data is succesfully fetched and dont show the next view if something went wrong?
Because you want the data fetching to be async you cannot return a value, because returning a value is sync (the current thread has to wait until the function returns and then use the value). What you want instead is to use a callback. So when the data is fetched you can do an action. For this you could use closures so your method would be:
func shouldPerformSegue(identifier: String, sender: AnyObject?, completion:(success:Bool) -> ());
And just call completion(true) or completion(false) in your session.dataTaskWithURL block depending on if it was successful or not, and when you call your function you give a block for completion in which you can perform the segue or not based on the success parameter. This means you cannot override that method to do what you need, you must implement your own mechanism.