Working on a practice project (solution found here: https://github.com/appbrewery/ByteCoin-iOS13-Completed) where you swipe with a picker view to see the value of 1 Bitcoin in the selected currency.
Right now, I'm successfully retrieving and parsing the data from coinapi.io, but my delegate method to update the text labels isn't activating and I can't figure out why, even comparing it to the solution code. I'm not getting any errors and it runs fine, but this update method just isn't calling. Why not?
ViewController.swift
// CoinManager Delegate Extension Functionality
extension ViewController: CoinManagerDelegate {
func didUpdateCoin(currency: String, value: String) {
// Test to see if it's being called
print("didUpdateCoin Called")
DispatchQueue.main.async {
// Change the information presented to the user
self.currencyLabel.text = currency
self.bitcoinLabel.text = String(value)
}
}
// If it fails, print the error that occurred
func didFailWithError(error: Error) {
print(error)
}
}
This is where I'm calling it.
CoinManager.swift
var delegate: CoinManagerDelegate?
func performRequest(with urlString: String, currency: String) {
// If the url is valid
if let url = URL(string: urlString) {
// Create the URLSession to request the data
let session = URLSession(configuration: .default)
// Create the task and session with the url
let task = session.dataTask(with: url) { (data, response, error) in
// If there's an error
if error != nil {
// Call the error-handling function
self.delegate?.didFailWithError(error: error!)
// Return without any request being performed
return
}
// If the data is retrieved successfully
if let safeData = data {
// Parse the data
if let value = self.parseJSON(safeData) {
print("Got to just before didUpdateCoin")
// The value is being passed correctly
print(value)
// Not calling, just skipping passed didUpdateCoin
self.delegate?.didUpdateCoin(currency: currency, value: value)
print("Passed didUpdateCoin")
}
}
}
// Continue running
task.resume()
}
}
Output
Got to just before didUpdateCoin
9768.79
Passed didUpdateCoin
The problem is - you have not connected your delegate with second view where you want to update content
For make it works follow next steps:
1) Create Protocol
2) Create delegate variable with your protocol type, you can make it optional
3) Chose where you want to call method from your delegate
4) In related VC assign it itself
5) Subscribe to protocol
6) Add protocol stubs
7) Configure your functionality
For better understanding: https://www.youtube.com/watch?v=DBWu6TnhLeY
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 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 building an app that populates data in a collectionView. The data come from API calls. When the screen first loads I get the products and store them locally in my ViewController.
My question is when should I get the products again and how to handle screen changing. My data will change when the app is running (sensitive attributes like prices) , but I don't find ideal solution to make the API call each time viewWillAppear is being called.
Can anybody please tell me what is the best pattern to handle this situation. My first though was to check if [CustomObject].isEmpty on viewWillAppear and then make the call. Including a timer that check again every 10-15 minutes for example.
Thank you for your input.
I'm not sure what the data looks like and how your API in detail works, but you certainly don't have to call viewWillAppear when your API updates the data.
There are two possible solutions to be notified when your data is updated.
You can either use a notification that lets you know whether the API is providing some data. After the Data has been provided your notification then calls to update the collection view. You can also include in the objects or structs that contain the data from your API the "didSet" call. Every time the object or struct is being updated the didSet routine is called to update your collection view.
To update your collection view you simply call the method reloadData() and the collection view will update itself and query the data source that now contains the newly received data from your API.
Hope this helps.
There is no set pattern but it is advisable not to send repeated network requests to increase energy efficiency (link). You can check the time interval in ViewWillApear and send the network requests after certain gap or can use timer to send requests at time intervals. First method would be better as it sends request only when user is on that screen. You can try following code snippet to get the idea
class ViewController: UIViewController {
let time = "startTime"
let collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
update()
}
private func update() {
if let startDateTime = NSUserDefaults.standardUserDefaults().objectForKey(time) as? NSDate {
let interval = NSDate().timeIntervalSinceDate(startDateTime)
let elapsedTime = Int(interval)
if elapsedTime >= 3600 {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(startDateTime, forKey: time)
}
} else {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(NSDate(), forKey: time)
}
}
func makeNetworkRequest() {
//Network Request to fetch data and update collectionView
let urlPath = "http://MyServer.com/api/data.json"
guard let endpoint = NSURL(string: urlPath) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
return
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
print("Error in json parsing")
return
}
self.collectionView.reloadData()
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}
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.
My share extension has the following code as part of the didSelectPost() segment:
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
print("DEBUG- We're done:\(success) and \(error)")
}
)
}
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
I'm getting the following error at runtime:
host connection <NSXPCConnection: 0x7fb84af2e8c0> connection from pid 16743 invalidated
I believe this error is due to the andCompletionBlock and related to the following issue: Debug info when run today extension
How can I cleanly and successfully deal with the completion status of the above transaction?
Like the answer you linked to stated, the NSXPCConnection error doesn't matter here.
The issue is that .runTransactionBlock() is asynchronous and .completeRequestReturningItems() will get called and exit the extension before you ever get a value from your Firebase database.
Try running .completeRequestReturningItems() in the andCompletionBlock.
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
)
}
}