I am building an app that calculates stuff.
The user will enter his/her inputs in OperationViewController and then he/she will click "Calculate". After calculating the results, a segue is performed to show the ResultsViewController.
Some of the calculations will take a long time, so I think they should be done in the background thread. And I should show a message saying that it is calculating and an activity indicator.
I also grabbed some code from somewhere that makes doing things in the background super swifty. Here is the code:
import Foundation
infix operator ~> {}
/**
Executes the lefthand closure on a background thread and,
upon completion, the righthand closure on the main thread.
Passes the background closure's output, if any, to the main closure.
*/
func ~> <R> (
backgroundClosure: () -> R,
mainClosure: (result: R) -> ())
{
dispatch_async(queue) {
let result = backgroundClosure()
dispatch_async(dispatch_get_main_queue(), {
mainClosure(result: result)
})
}
}
/** Serial dispatch queue used by the ~> operator. */
private let queue = dispatch_queue_create("serial-worker", DISPATCH_QUEUE_SERIAL)
Then, the problem arises.
In OperationViewController, there is a method called getResults:
private func getResults () -> [(name: String, from: String, result: String)]? {
// irrelevent code about getting the user's inputs from UITextFields
return operation.calculate(input) // This will take a few seconds
}
The calculate method there requires a few seconds to return.
The Calculate button, mentioned earlier has a segue that's connected to ResultsViewController. I didn't explicitly call performSegueWithIdentifier. I just control dragged the button to the results view controller.
In prepareForSegue, I call getResults:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showOperationHelp" {
// irrelevent
} else if segue.identifier == "showResults" {
let vc = segue.destinationViewController as! ResultsViewController
vc.results = getResults()
}
}
Now I tried to put the getResults part in a background thread by doing this:
} else if segue.identifier == "showResults" {
let vc = segue.destinationViewController as! ResultsViewController;
{ self.getResults() } ~> { vc.results = $0 };
}
But that doesn't work because prepareForSegue returns before the calculation finish. This means that vc.results is nil when prepareForSegue returns. This will cause the ResultsViewController to show nothing.
Another method I have tried is to put the "do in background" thingy in getResults:
private func getResults () -> [(name: String, from: String, result: String)]? {
// irrelevent code about getting the user's inputs from UITextFields
var results: [(name: String, from: String, result: String)]?;
{ self.operation.calculate(input) } ~> { results = $0 };
return results
}
Again, getResults will just return nil.
Back then when I was writing C#, I can use the async/await keywords to achieve this.
I can just do something like:
var results = await operations.Calculate(input);
When execution reaches await, it pauses there and allows the UI thread to go on. After the async operation has finished, the execution goes back to where it had stopped and continues on.
Question: Can I do something like the above in Swift? If I can't, how can I wait until the calculations finish and show ResultsViewController?
Just in case you didn't understand me, I'll describe exactly what I want:
the user enters some inputs
the user taps the calculate button
A message shows up telling the user that it is calculating
Some time later
Calculation finishes
ResultsViewController is shown
After some trying, I can only get this:
the user enters some inputs
the user taps the calculate button
A message shows up telling the user that it is calculating
ResultsViewController is shown, with no results
Some time later
Calculation finishes but ResultsViewController don't know.
P.S. I don't know how to improve the question title...
Editing original response for clarity:
Have the the calculate button call a method called calculate() or something that shows the activity indicator, then calls the getResults method.
give getResults() a completionHandler, and on successful completion dismiss the activity indicator and perform the segue with the completed calculation.
Related
I am writing a chess GUI in Swift 3 and use nvzqz/Sage as the chess model/library. Now I face a problem with a Sage closure used for piece promotion.
Sage uses (in its game class) the execute(move: promotion:) method for promotion move execution which has a closure that returns a promotion piece kind. This allows to prompt the user for a promotion piece or perform any other operations before choosing a promotion piece kind, as follows:
try game.execute(move: move) {
...
return .queen
}
I implemented a promotion view controller ("pvc") which is called in this closure so that the player may select the new piece:
// This is in the main View Controller class
/// The piece selected in promotionViewController into which a pawn shall promote
var newPiece: Piece.Kind = ._queen // default value = Queen
try game.execute(move: move) {
boardView.isUserInteractionEnabled = false
// promotionview controller appears to select new promoted piece
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
return newPiece
}
When the button for the new piece in the pvc is pressed, the pvc dismisses itself and the data of the selected piece (the constant selectedType) is transferred back to the main view controller via delegation:
// This is in the sending PVC class
protocol PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind)
}
func buttonPressed(sender: UIButton) {
let selectedType = bla bla bla ...
delegate?.processPromotion(selectedType: selectedType)
presentingViewController!.dismiss(animated:true)
}
// This is in the receiving main View Controller class
extension GameViewController: PromotionViewControllerDelegate {
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
}
The problem I have is that the closure (in the game.execute method) does not wait until the player made his selection in the pvc (and immediately returns the newPiece variable which is still the default value) so that I never get another promotion piece processed other than the default value.
How do I make the closure wait until the player pressed a button in the pvc?
Of course, I tried to find a solution and read about callbacks, completion handlers or property observers. I do not know which is the best way forward, some thoughts:
Completion handler: the pvc dismisses itself upon button-press event so the completion handler is not in the receiving (main) view controller. How do I deal with this?
Property observer: I could call the try game.execute(move) method only after the promotion piece was set (with didset) but that would make the code difficult to read and not use the nice closure the game.execute method provides.
Callbacks may be related to completion handlers, but am not sure.
So your block in game.execute(move: move) will fully execute which is so designed by the Sage API. You can not pause it as easy but it is doable, still let's try to solve it the other way;
Why do you need to call the presentation of the view controller within this block? By all means try to move that away. The call try game.execute(move: move) { should only be called within processPromotion delegate method. You did not post any code but wherever this try game.execute(move: move) { code is it needs to be replaced by presenting a view controller alone.
Then on delegate you do not even need to preserve the value newPiece = selectedType but rather just call try game.execute(move: move) { return selectedType }.
So about pausing a block:
It is not possible to directly "pause" a block because it is a part of execution which means the whole operation needs to pause which in the end means you need to pause your whole thread. That means you need to move the call to a separate thread and pause that one. Still this will only work if the API supports the multithreading, if the callback is called on the same tread as its execute call... So there are many tools and ways on how to lock a thread so let me just use the most primitive one which is making the thread sleep:
var executionLocked: Bool = false
func foo() {
DispatchQueue(label: "confiramtion queue").async {
self.executionLocked = true
game.execute(move: move) {
// Assuming this is still on the "confiramtion queue" queue
DispatchQueue.main.async {
// UI code needs to be executed on main thread
let pvc = PromotionViewController(nibName: nil, bundle: nil)
pvc.delegate = self
pvc.modalPresentationStyle = .overCurrentContext
self.present(pvc, animated:true)
}
while self.executionLocked {
Thread.sleep(forTimeInterval: 1.0/5.0) // Check 5 times per second if unlocked
}
return self.newPiece // Or whatever the code is
}
}
}
Now in your delegate you need:
func processPromotion(selectedType: Piece.Kind) {
defer {
boardView.isUserInteractionEnabled = true
}
newPiece = selectedType
self.executionLocked = false
}
So what happens here is we start a new thread. Then lock the execution and start execution on game instance. In the block we now execute our code on main thread and then create an "endless" loop in which a thread sleeps a bit every time (the sleep is not really needed but it prevents the loop to take too much CPU power). Now all the stuff is happening on main thread which is that a new controller is presented and user may do stuff with it... Then once done a delegate will unlock the execution lock which will make the "endless" loop exit (on another thread) and return a value to your game instance.
I do not expect you to implement this but if you will then ensure you make all precautions to correctly release the loop if needed. Like if view controller is dismissed it should unlock it, if a delegate has a "cancel" version it should exit...
Let's say I have a ViewController A and a class B.
when I press some button inside A, it calls an IBAction that calls a function B.foo() which returns an Int
B.foo() takes 8~10 seconds to finish and while it runs I'd like to put an Loading... animation on A, and when B.foo() finishes, the animation would stop.
How can I do this? this is an pseudo-code example of what I wish:
#IBAction func buttonPressed(_ sender: UIButton){
UIView.animate(blablabla......)
DO({
self.answer = B.foo()
}, andWhenItFinishesDo: {
self.someone.layer.removeAllAnimation()
})
}
This is a very common problem. One way to solve it would be to use different queues (You can think of them as lines of work that can happen in parallel).
The the basic idea is that once your button is pressed, you show your loading indicator and "dispatch" the long work to a secondary queue, that will operate in the background and do the work. This ensures that your main queue does not block while the work happens and the user interface stays responsive.
The trick is now that you want to get notified when the long work is finished so that you can stop showing the loading indicator (and possibly do even more).
While you actually could use some kind of notification system, there are other, sometimes more appropriate ways. It would actually be even more convenient, if you could just tell the long running function to call you back specifically with code that you provide.
That would be the basic concept of a "completion handler" or "callback".
The whole thing would look something like that:
// Some made up class here
class B {
// This is only static because I do not have an instance of B around.
static func foo(completion: #escaping (Int) -> Void ) {
// The method now does all of its work on a background queue and returns immediately
DispatchQueue.global(qos: .background).async {
// In the background this may take as long as it wants
let result = longCalculation()
// VERY important. The caller of this function might have a certain
// expectation about on which queue the completion handler runs.
// Here I just use the main queue because this is relatively safe.
// You could let the caller provide a queue in the function
// parameters and use it here
DispatchQueue.main.async {
// The completion handler is a function that takes an Int.
// That is exactly what you are providing here
completion(result)
}
}
}
}
#IBAction func buttonPressed(_ sender: UIButton){
self.showLoadingIndicator()
// The foo function now takes a completion handler that gets the result in.
// You have to provide this function here and do something with the result
//
// The completion handler will only be run when the foo function calls it
// (which is after the computation as you can see in the method above.
//
// I am also telling the completion handler here that self should not be
// held on to as the view controller might already have gone away when the
// long calculation finished. The `[weak self]` thingy makes that inside
// your completion handler self is an optional and might be nil (and it
// doesn't hold a strong reference to self, but that's a whole other topic)
B.foo(completion: { [weak self] result in
// Do something with the result
// Since we are called back on the main queue we can also do UI stuff safely
self?.hideLoadingIndicator()
})
}
I hope this helps a bit.
Asynchronous programming can be quite difficult to learn but there are tons of tutorials and examples you can find on this topic.
Hey Hamish you can do this in two simple ways,
First one is using the defer statements provided for functions.
Defer statement block is executed after the functions goes out of scope.
here is a simple example to describe the same.
func print1000000() {
//start displaying the loading indicator
defer {
// hide the loading indicator and move to the next ViewController
let seVC = storyboard?.instantiateViewController(withIdentifier: "SecondVC") as! SecondVC
self.navigationController?.pushViewController(seVC, animated: true)
}
// here goes the task you want to execute such as downloading a file or the one i did here
for index in 0...1000000 {
print(index)
}
}
The above function prints numbers upto 1000000 and then pushes the control to another ViewController
=========================================================================
Second way of doing it is by using closures, as described by Thomas in his answer.
I am trying to detect clicks on an UIElement like a button using Reactive Cocoa, using RAC for the first time in MVVM architecture.
I set the rac_command for my button in my ViewController.
addContactBtn.rac_command = viewModel.addContact
My ViewModel does the following:
func init(){
self.addContact = RACCommand() {
(any:AnyObject!) -> RACSignal in
return RACSignal.createSignal({
(subscriber: RACSubscriber!) -> RACDisposable! in
print("creating viewModel")
let viewModel = ContactAddViewModel(services: self.services)
self.services.pushViewModel(viewModel)
return RACDisposable(block: {
})
})
}
}
However, the command is executed only once and then the button is in disabled state when I pop the view controller and come back to original viewController. How can detect the button click any number of times?
Obviously, you missed something and had a simple mistake.
RACCommand expects to a signal which will be alive when the button clicked and be disposed after click-business-logic (like create viewModel, then pushViewModel in the above) executed. That is to say each button clicking-event associates a signal, not shares one unique signal, but has common signal inner logic. If a signal does not achieve completion or error, the responding clicking-event is not finished such that the button is disabled.
The below modified codes could be correct.
func init(){
self.addContact = RACCommand() {
(any:AnyObject!) -> RACSignal in
return RACSignal.createSignal({
(subscriber: RACSubscriber!) -> RACDisposable! in
print("creating viewModel")
let viewModel = ContactAddViewModel(services: self.services)
self.services.pushViewModel(viewModel)
// if you like to expose some value
// subscriber.sendNext(value)
subscriber.sendCompleted() // which makes clicking-event finished
// maybe error occurs
// subscriber.sendError()
return RACDisposable(block: {
})
})
}
}
I would like to advise you to checkout CocoaAction and Action in ReactiveSwift, which are replacement for RACCommand of legacy ReactiveObjC.
I'm trying to run a url request to get a JSON file after a certain table row is selected, based on the row a unique ID is sent with the URL request and a different JSON is generated. Here is my prepareforSegue
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
var divisionScene = segue.destinationViewController as! DivisionViewController
// Pass the selected object to the new view controller.
if let indexPath = tableView.indexPathForSelectedRow() {
let arrayIndex = indexPath.row
//println("Index: \(arrayIndex)")
torneoIDTransfer = torneos[arrayIndex].torneoID
//println("\(torneoIDTransfer)")
//check second url with second request type same token
//sets url to string using token
let tercerURL = NSURL(string: "http://api.zione.mx/get.json.asp?tr=3&tkn=\(tkn)&tor=\(torneoIDTransfer)")
//initializes request
let request = NSURLRequest(URL: tercerURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.currentQueue()) { response, jsonDataRequest3, error in
let dataRequest3 = jsonDataRequest3
//takes data, saves it as json
let tercerJSON = JSON(data: jsonDataRequest3)
//checks to see that contents != nil, meaning the JSON file was found
if tercerJSON != nil {
//checks amount of tournaments available in order to generate table.
let divisionCount = tercerJSON["lista-divisiones"].count
//sets global variable numero de torneos
numeroDeDivisiones = divisionCount
//for loop to go insert into Torneo nuevo each ID and name by using count from above
for var index = 0; index < divisionCount; ++index {
var divisionID = Int(tercerJSON["lista-divisiones" ][index]["DivisionID"].number!)
var nomDivision = tercerJSON["lista-divisiones"][index]["nomDivision"].string
//println("\(divisionID)")
//println("\(nomDivision)")
var divisionNuevo = listaDivisiones(divisionID: divisionID, nomDivision: nomDivision!)
divisiones.append(divisionNuevo)
numeroDeDivisiones = 10
print("WHO IS FIRST")
}
}
print("\(divisiones[0].nomDivision)")
}
}
}
And I created my segway by dragging from the table cell to the new view Controller. However when I click the table cell the transition occurs instantly, before the request has a chance to finish and as a result no data is displayed.
It would be ideal to fetch and process the data in the background, long before the user ever selects a table row. If this is not possible, then I would suggest having your destination view controller do the URL request. The URL request happens asynchronously, so it will never have a chance to finish before your source view controller is deallocated.
In your source view controller, modify prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var divisionScene = segue.destinationViewController as! DivisionViewController
if let indexPath = tableView.indexPathForSelectedRow() {
let arrayIndex = indexPath.row
torneoIDTransfer = torneos[arrayIndex].torneoID
let tercerURL = NSURL(string: "http://api.zione.mx/get.json.asp?tr=3&tkn=\(tkn)&tor=\(torneoIDTransfer)")
divisionScene.fetchDataAtURL(tercerURL)
}
}
You'll need to define a method inside your destination view controller to handle the fetching.
func fetchDataAtURL(URL: NSURL) {
let request = NSURLRequest(URL: tercerURL!)
NSURLConnection.sendAsynchronousRequest( // ... your fetching logic here
}
You'll also need some logic to display the data once it arrives. I would suggest putting it into your request's completion callback (or rather, having the callback trigger a display update). If your divisionScene is a tableView, you might be able to call reloadData (after you update the data source). If not, you'll need some other way to update the UI. If you are updating the UI, make sure to dispatch to the main queue for that part.
Doing this way at least passes the URL loading responsibility to the destination view controller, which will at least be around when the data finally gets there.
" the transition occurs instantly, before the request has a chance to finish".
Of course it does, that is exactly what asynchronous means. You make a point of mentioning it is asynchronous but you must have a misunderstanding about what that means. Explain what you think it means and what you expected your code to so so that you can be better educated by us.
When you call sendAsynchronousRequest() think of your program as branching into two (actually that is what does effectively happen). One part is your original code which will continue to execute i.e your prepareFoSegue code will continue to execute.
Meanwhile, in parallel, the OS will execute the request, and when the request has finished the code in the block that you passed to sendAsynchronousRequest() will be executed. Therefore your prepareForSeque() function will finish before the Json has been received.
But apart from all that, you should not be attempting or hoping or wanting the JSon to be fetched before the segue transition - to do this would halt your ui. Suppose sendAsynchronousRequest() was instead sendSynchronousRequest() and it took 10 seconds to complete, what do you think the consequence would be on your app when it runs?
You should either fetch your data a long time before you GUI is ready to display it, or if that is not possible, display your GUI immediately with no data and then update it as the data arrives.
I've browsed through many q&a-s here but almost all of them are way too specific for the use cases of other people.
My situation is more general and sort of simple:
iOS\Swift
I have a button which when clicked - moves the user to the next view.
At the same time as the button is clicked it also executes a query to Parse to fetch the data which will be displayed on the next view.
I'm using Parse's async query.getObjectInBackgroundWithId("jjjkkkdddd")
So if my code runs as is = click -> move to the next view -> empty
because fetching stuff takes a second or so.
What i want is to have a small animation popping up when user clicks a button to tell them that the data is being fetched at the moment and move to the next view once data arrives.
Here is my button tap code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "1") {
var svc = segue.destinationViewController as ViewControllerQuotes;
svc.toPass = functionToFetchDataFromParse() //<- takes longer than
//switching to the next view
//toPass is a var which lands into the next view
//and it's value is displayed to the user.
}
}
I am quite fresh with Swift and iOS dev so I can't figure this one out still:(
On the button tap just use addSubview() to add a UI element like UIActivityIndicatorView.
In the callback from Parse, remove the view and go to the next controller, calling the segue programatically.
SOLUTION FOR ME:
Ok i fiddled around and found the best way which suites me for now. what I do is from View1 the button is pressed and it tell the view controller of the 2-nd view to fire up the data fetching function.
In the 2-nd vie controller once it trigger the async function for retrieving data from Parse it also shows the activity indicator and make it run.
The user sees an empty screen with an activity spinner running. once the async function gets back the data it pushes it into a an empty label and shutsdown+hides the activity indicator.
Works great for my case. no interface hanging. and clear for the user about what's happening where and when.
Thanks to everyone!
p.s. this works for me because that's the end of the lifecycle for the retrieved data. if there were other functions depending on it - this wouldn't work:(
What about setting the object on the segue's destination View controller and then fetch the object, and then reload the view?
This would be:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "1") {
var svc = segue.destinationViewController as ViewControllerQuotes;
svc.yourObjectWhichIsToFetch = yourObjectWhichYouWantToPass //need to create a variable on the ViewControllerQuotes!
}
}
And then in the ViewControllerQuotes class:
class ViewControllerQuotes:ViewController {
var yourObjectWhichIsToFetch:PFObject?
override func viewDidLoad(){
super.viewDidLoad()
yourObjectWhichIsToFetch.fetchInBackgroundWithBlock({
(object, error) -> Void in
if error == nil {
//update the UI
}
})
}
}
With this code, you don't need to show any loading note (what is really ugly).