I am using Swift 3, Xcode 8.2.
I have a view controller on which there is a button. When the button is pressed, it runs the buttonPressed function below. It kicks off an asynchronous job which takes a few seconds to complete during which a spinning gear activity indicator pops up. Once the job is finished, I want the activity indicator to dismiss and the view controller to dismiss as well to the VC that it originally came from.
func buttonPressed() {
// Set up some stuff here
...
// this code block here is to asynchronously run the processing job
// while the activity indicator gear spins
self.displaySpinningGear()
DispatchQueue.main.async {
self.doSomeJobProcessing()
}
self.dismiss(animated: true, completion: nil)
// also dismiss the camera view
self.presentingViewController?.dismiss(animated: true, completion: nil) // error here
}
However, I get an error on that last statement:
[Assert] Trying to dismiss the presentation controller while
transitioning already.
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
2017-03-03 21:37:56.833899 EOB-Reader[27710:6869686] [Assert]
transitionViewForCurrentTransition is not set, presentation controller
was dismissed during the presentation?
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
Any help would be greatly appreciated.
Perform anyone transition of these at a time.
if let viewController = presentingViewController {
// This block will dismiss both current and a view controller presenting current.
viewController.dismiss(animated: true, completion: nil)
} else {
// This block will dismiss only current view controller
self.dismiss(animated: true, completion: nil)
}
Two transition operations cannot be performed simultaneously.
In your source code you are dismissing current view controller with animation. Now animation generally takes time around 0.25 second to complete its operation.
Now, in next line you are trying to dismiss a ( ** ) view controller that has presented current view controller. (In your case ( ** ) view controller is also presented by some other view controller.) so, within a fraction of seconds/milliseconds ( ** ) view controller will also try to dismiss it self with animation.
At this time your current view controller is being dismissed and (**) view controller will also start dismiss operation. So, both operations conflict each other on main executing thread. And results into an issue, you are facing.
Also, share your code for block
self.doSomeJobProcessing()
Share here, if you've set any other view/controller transition operations here.
Remember that when you dispatch asynchronous you're typically dispatching to background thread to prevent UI blocking. All on screen (UI) components need be modified on the main thread. So start your progress spinner from the main thread, dispatch your heavy lifting to the background, then redispatch to the main thread that you'd like to. this.dismissviewcontroller:animated
Will update with code shortly.
Related
I'm using material design dialog for my iOS app written with swift. Here is the brief documentation of material design dialogs: https://material.io/develop/ios/components/dialogs/
I have a dialog which has 1 action and in the completion block of the action, I want to dismiss the view controller and go back to the previous view controller. The problem is that dismissing the view controller doesn't work. All instructions which are written in the completion block, such as printing something, execute except for dismissing view controller.
Here is my code :
DispatchQueue.main.async {
let alertStr = "Alert"
let alertController = MDCAlertController(title: "Error", message: alertStr)
let action = MDCAlertAction(title:"GoBack") { (action) in
self.dismiss(animated: false, completion: nil)
}
alertController.addAction(action)
self.present(alertController, animated:true, completion:nil)
}
I'd appreciate if you could help me figure out the problem.
Thanks in advance !
A couple of thoughts:
The dismiss(animated:completion:) “Dismisses the view controller that was presented modally by the view controller.” It’s not intended to dismiss the the view controller referenced by self.
Admittedly, dismiss will, “If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.” But you can’t rely upon that within the UIAlertAction for the button, because you don’t know when the dismissal of the MDCAlertController and when the action of the button is performed.
Are you sure you presented view controller and that it’s not, for example, having instead pushed on a navigation controller?
A good way of getting back to a prior view controller is an unwind segue (or see TN2298). That eliminates all ambiguity about “push” v “present” and whether dismiss will dismiss a presented view controller and instead pass the message to the presenting view controller.
have you tried to
performSegue(withIdentifier: "ViewControllerSegue", sender: nil)
you need to select your viewController on the top-bar the yellow square(name is what you predefine)
right-click and drag to the next view controller ---> Present Modally
then select the arrow and go to attributes inspector and name the identifier.
I'm using snapshot testing for my view controller. This is how the view controller is initialized in tests:
window.addSubview(viewController.view) // simulate the view is visible (probably unnecessary)
viewController.view.frame = self.snapshotFrame // set frame
viewController.beginAppearanceTransition(true, animated: false) // simulate VC's life cycle
viewController.endAppearanceTransition()
My view controller contains UICollectionView. When I perform collection view updates using performBatchUpdates, even though the update block is finished, the completion is never called.
// Animate udpates
self.collectionView.performBatchUpdates({
// is called
}, completion: { _ in
// never called
})
I think it's related to the off screen rendering of the collection view. Does someone have any experience with similar issue? What am I missing to convince UICollectionView that it's on screen?
I found the problem. It was all about a proper timing. The test case finished before the completion was called and the view controller was deallocated.
I speeded up collection view animations by setting
viewController.view.layer.speed = 100 // speed up animations
and increased timeout for the test case to 0.1 seconds. Everything works as expected now.
I have a storyboarded app using segues without a navigation controller. We have a simple custom segue like the code below that works fine for normal cases. However we have a situation where after invoking a chain of view controllers A->B->C we’d sometimes like C to unwind directly back to A. This works fine with the built-in transitions as UIKit seems to dismiss the intervening controller without any trouble, properly showing a transition from C back to A.
However in my custom segue I am not certain if or how I should dismiss the intervening view controllers. Assume that the code below knows both that it is unwinding and that the destination view controller is not the one presenting the source view controller. Here is what I’ve tried:
1) If I simply leave the code as is seems to work fine! However I get errors “Unbalanced calls to begin/end appearance transitions” for view controller C.
2) If I change the code to dismiss view controller B first I see the transition to B. I don’t see any way to suppress this.
From what I've read the unbalanced calls error comes up often when you have overlapping calls to transitions. However I don't see how that could be the case here since UIKit is dismissing them (presumably). I've also tried dispatching the completion work to the main thread (to try to get it into the next run loop) but no change.
class MyCustomSegue: UIStoryboardSegue {
override func perform()
{
let sourceView = sourceViewController.view
let destView = destinationViewController.view
let unwinding = // …
// Put the destination view on top
sourceView.superview?.insertSubview(destView, aboveSubview: sourceView)
// Do the animation
UIView.animateWithDuration(1.0, animations: { complete in
} ) { (Finished) -> Void in
if unwinding {
// calling this on presenter or presentee is equivalent
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
} else {
self.sourceViewController.presentViewController(self.destinationViewController, animated: false, completion: nil)
}
}
}
}
From my initial View Controller, I want to be able to segue to another view controller. This second view controller, however, takes a few seconds to load, and I want to have my source view controller display an animation while it loads.
Performing the segue blocks the rest of the code from running, but I can't put it in a background thread because it technically updates the UI (Xcode gave me lots of warnings when I tried).
The animation also updates the UI, which means I can't put that in its own separate thread either.
Is there another method I could use to accomplish this?
class LaunchViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(5, animations: {
//Perform animation that updates the UI
}, completion: nil)
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("seg", sender: self)
}
}
P.S. When I put performSegueWithIdentifier in a background thread, it worked perfectly. But like I said, I get lots of errors (and Xcode will throw an exception in a coming release).
I am working on an app with multiple view controllers. I have a main menu view controller, game view controllers with a timer and one game over view controller.
Main menu has a button that takes you to game view controller. There is a timer that starts and when it reaches zero it automatically takes you to game over view controller.
The problem is that if I am in the game view controller and decide to go back to main menu the timer continues.
I then get a warning message:
Warning: Attempt to present <...> on <...> whose view is not in the window hierarchy!
How do I get the timer to stop when I change view controller?
invalidate the timer before the view disappears:
override func viewWillDisappear(animated: Bool) {
timer.invalidate()
timer = nil
super.viewWillDisappear(animated)
}