How to stop a previous viewController's activity after being popped - ios

I have a navigation controller that segues, using push, to a second view controller. I use:
controller.navigationController!.popToRootViewController(animated: true)
And this works. This code causes my controller to be popped off the stack and it returns to the root view. However, I can still hear my code running (e.g. music/sound effects) in the root view.
I have explored a few avenues.
One is to create a function that runs when my back buttons is pressed. Currently, it removes all the actions/pauses all the music:
func pauseAndRemoveEverything() {
for child in fgNode.children { child.removeAllActions() }
for child in bgNode.children { child.removeAllActions() }
soundEffectsPlayer?.volume = 0
musicPlayer?.volume = 0
soundEffectsPlayer?.stop()
musicPlayer?.stop()
}
The second option is to use multi-threading and dispatching little blocks of code; then cancelling them all when the user hits the back button. Currently my code is all on timed blocks, and I don't know how the dispatch will affect this, or whether it is more trouble than it is worth.
I want the actions on the previous controller to completely cease on segue, and reduce the effect it has on memory and energy use when this occurs. How do I do this?

Related

How can I force update my UI immediately in Swift 5 using UIKit?

I ran into a problem with my UI, which is not updating immediately.
I am calling someCustomView.isHidden = false first. After that I create a new instance of a new View Controller. Inside the new VCs viewDidLoad(), I am loading a "new Machine Learning Model", which takes some time.
private func someFuncThatGetsCalled() {
print("1")
self.viewLoading.isHidden = false
print("2")
performSegue(withIdentifier: "goToModelVCSegue", sender: nil)
}
As soon as I press the button that calls this function, "1" and "2" is printed in the console. However the view is not getting visible before the viewDidLoad() of my new VC is finished.
Is there any possibility to force update a UIView immediately? setNeedsDisplay() did not work for me.
Thanks for your help!
Use layoutIfNeeded() Apple Docs
layoutIfNeeded()
Lays out the subviews immediately, if layout updates are pending.
Use this method to force the view to update its layout immediately. When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. Using the view that receives the message as the root view, this method lays out the view subtree starting at the root. If no layout updates are pending, this method exits without modifying the layout or calling any layout-related callbacks.
So As a rule of thumb,
layoutifneeded : Immediate (current update cycle) , synchronous call
setNeedsLayout :relaxed ( wait till Next Update cycle) , asynchronous call
So, layoutIfNeeded says update immediately please, whereas setNeedsLayout says please update but you can wait until the next update cycle.
how to use
yourView.layoutIfNeeded()
You can also refer to the diagram to better remember the order of these passes
Source Apple docs on layoutIfNeeded
Image credit Medium artcle
A couple problems...
If you have a view controller that "takes some time" to load, you should not try to do it in that manner.
The app will be non-responsive and appear "frozen."
A much better approach would be:
on someFuncThatGetsCalled()
hide viewLoading and replace it with an activity indicator (spinner, or something else that let's the user know the app is not stuck)
instantiate your ModelVC
when ModelVC has finished its setup, have it inform the current VC (via delegate)
current VC then shows / navigates to the already instantiated and prepared ModelVC
Or, probably a better option... Move your time-consuming setup in ModelVC to a point after the view has appeared. You can show an activity indicator in viewDidLoad(). That is really the most common UX - you see it all the time when the new VC has to retrieve remote data to display - and it would fit wit what users have come to expect.

Checking the current view state after block/closure completion

Within a asynchronously executed block/closure, I want to get a check on my current state before I executed anything within that block.
A common example of where this presents itself is segueing to the next View Controller after a NSURLsession request.
Here's an example:
#IBAction func tappedButton(sender: UIButton) {
//This closure named fetchHistorical goes to the internet and fetches an array
//The response is then sent to the next view controller along with a segue
Order.fetchHistorical(orderID, completionHandler: { (resultEnum) -> () in
switch resultEnum {
case .Success(let result):
let orderItemsArray = result.orderItems!.allObjects
self.performSegueWithIdentifier("showExchanges", sender: orderItemsArray)
default:
let _ = errorModal(title: "Error", message: "Failed!")
}
})
}
Assume that the user has been impatient and tapped this button 3 times.
That would mean this function will be called three times and each time it would attempt to segue to the next view controller (iOS nicely blocks this issue with "Warning: Attempt to present on whose view is not in the window hierarchy!")
I wanted to know how do you folks tackle this problem? Is it something like ... within the closure, check if you are still in the present viewcontroller ... if you are, then segueing is valid. If not, you probably have already segued and don't execute the segue again.
***More generally, how are you checking the current state within the closure because the closure is executed asynchronously?
Since the closure isn't executing on the main thread the state would be in accurate if you check it here (as you stated). You can use GCD to go to the main thread and check the state there. There are a couple of ways you can keep this code from running multiple times. If it will take some time to perform the calculations you can use an acitivity indicator to let the user know the app is busy at the moment. If you want the user to still have the option of pressing the button you can put a tag like:
var buttonWasTapped:Bool = false //class property
#IBAction func tappedButton(sender: UIButton) {
if !self.buttonWasTapped{
self.buttonWasTapped = true
}
}
Then change it back to false on viewDidAppear so they can press once every time that page is shown.
When starting some task that will take some time to complete I would do two things;
Show some sort of activity indicator so that the user knows something is happening
Disable the button so that there is further indication that the request has been received and to prevent the user from tapping multiple times.
It is important that you consider not only the correct operation of your app but also providing a good user experience.

Simultaneous NSURLConnection from within prepareForSegue

I have a UITableView that requests for more data from the server when the user hits the bottom of the table (similar to the Twitter application). However, I'm trying to use a modal segue to filter out data to the user's desire. In order to properly select which data to filter, I have to load ALL of the data to categorize it. In order to load everything, I am required to send out multiple NSURLConnections to load multiple pages. I am trying to have it so when one completes, the next one starts.
However since the connection completes with connectionDidFinishLoading, I have not figured out a way to send out simultaneous NSURLConnections from within prepareForSegue. I tried using a while loop in prepareForSegue as follows:
while (All of the data is not loaded) {
if (isLoading == NO) {
[self loadMoreResults];
}
}
where "isLoading" is a BOOL declared in my viewcontroller's implementation file. isLoading changes value to YES inside loadMoreResults, and changes back to NO at the end of connectionDidFinishLoading. However, within prepareForSegue, isLoading never changes back from YES to NO.
Is this a multithreading issue? I have done research on other questions and see that NSURLConnection has a class method sendAsynchronousRequest:queue:completionHandler: where the completion handler might help, but I'm unsure how I would use it.
ALSO: I want to continue executing prepareForSegue AFTER the last connection finishes, not right after it sends the request.
Thanks in advance!
Then you should not link the segue directly from the bar button item to the view controller in the storyboard.
Just link a general segue with identifier from the table view controller to the filter view controller.
And from the bar button item, create an action for it from the storyboard so that you can send out multiple NSURLConnections first.
Finally then in your code, after the last connection finishes, call the performSegueWithIdentifier method.

How can I add new pages to a UIPageViewController after the user has reached the last page?

I have a UIPageViewController that contains view controllers that are instantiated from data fetched over the network. When the user is paging and gets within a few pages of the end, I kick off a new network operation and put the resulting new view controllers onto the end of my UIPageViewControllerDataSource's mutable array of view controllers. In general this all works pretty well.
But if the network is slow (or the user is fast), it's possible to reach the final page before the network operation has completed. Since pageViewController:viewControllerAfterViewController: gets called when the user reaches the last page, it has already returned nil ("to indicate that there is no next view controller").
Is there a good way to force the UIPageViewController to call pageViewController:viewControllerAfterViewController: again (so that when the network operation completes and there are now new pages, I can force this call so the user can page to the new pages)? Or is there a workaround (maybe preventing the user from paging to the last page, but still showing the bounce animation)?
See here: Refresh UIPageViewController - reorder pages and add new pages
When you get your data back from the network, call setViewControllers: direction: animated: completion: and set the current view to the current one again. That forces it to re-call pageViewController:viewControllerAfterViewController:
ssloan's answer was part of the key, but in my case simply calling setViewControllers:direction:animated:completion: in the network operation completion block wasn't enough (due to some edge cases).
My solution was to store the most recent view controller for which pageViewController:viewControllerAfterViewController: returned nil (I called it endPage) and to create a method...
- (void)fixAndResetEndPage {
if (self.endPage && self.endPage == self.currentPage) {
[self.pageController setViewControllers:#[self.currentPage] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
self.endPage = nil;
}
}
...which I called whenever my NSMutableArray of content controllers was mutated (e.g., network operation completion) and in pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: whenever completed was YES.

Monotouch - UIView.Animate does nothing in a subview why?

I have a virtual function called DoRotate() that does an animation of a subview rotating. Pretty simple... It is fired from a timer event, and is always invoked on the main thread.
This works like a charm...
Now, I sub-class the whole view, and create another view that tries to do the same thing. It is displayed as a sub-view (full screen) of the main view... Now, UIView.Animate() is a No-op.. It never executes the code inside the block at all. It is actually the EXACT same function that is being called in the other view... It is attempting to animate a sub view (a sub-sub-view of the original view...
But it is like the OS is just saying "too bad, so sad, we are not going to work anymore.." - which is typical of the OS,but that is another topic...
Originally, I tried to just make this view another view that would flip over to from the base view, but apparently, without a navigation controller it won't work, and if you have one then the popup menu that I created will no longer popup...
UIView.Animate (tm, 0, UIViewAnimationOptions.CurveLinear, () =>
{
MapRotating = true;
_DoRotate ();
},
() => {
MapRotating = false;});
}
The MapRotating = true; never gets called.. I've put this before the animate call, but then it never tries again if the animate fails... This way, it keeps failing forever...
_DoRotate computes the angle to rotate and rotates it (sets a transform)...
Is there a UIView.LastError() method that I can call for it to tell me WHY is it Ignoring my request... (ooh, how about throwing an exception if we passed it something it didn't like?)
-Chris

Resources