I have two storyboards, a main then a second storyboard which houses a UIPageViewController. Inside the second storyboard, an exit button allows a user to return to the home screen which is on the main storyboard.
When I attempt to segue back to the home screen, the page controller views are stacking. I have tried to use:
self.navigationController?.popViewController
Along with some other methods to deallocate the UIPageViewController sets. Nothing seems to work? How do I fix this?
You can see in the image below where the controllers are marked by (3).
Memory Graph Image
Below are a few of my attempts. I am using a notification to invoke a method on the root controller after the user confirms via a yes/no custom modal.
func attempt1() {
self.viewControllerList.forEach {
index in
index.removeFromParentViewController()
index.dismiss(animated: false, completion: nil)
index.navigationController?.popViewController(animated: false)
}
self.performSegue(withIdentifier: "unwindHome", sender: self)
}
func attempt2() {
self.childViewControllers.forEach {
c in
print("CHILD...>", c)
c.removeFromParentViewController()
c.dismiss(animated: false, completion: nil)
}
}
func attempt3() {
(view.window?.rootViewController as? UIPageViewController)?.dismiss(animated: true, completion: nil)
self.dismiss(animated: false, completion: {
self.viewControllerList.forEach {
i in
i.navigationController?.popViewController(animated: true)
}
})
self.performSegue(withIdentifier: "unwindHome", sender: self)
}
Here were my issues:
On the first storyboard, my nav controller was not set to initial.
After setting it to initial, i then linked from the main SB to the second SB
Delegates var's were not set to weak
RxSwift was not being disposed of properly.
I was at first doing pub/sub via Variables as a Global. When moving the observer into the root of my UIPageViewController, upon page breakdown the observers were disposed of causing the onComplete callback to hit.
Related
In Start Developing iOS Apps (Swift): Implement Edit and Delete Behavior
, The offical tutorial tell me should use presentingViewController and navigationController to indicate the specified view is Edit or Show, Like following code:
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddMealMode = presentingViewController is UINavigationController
if isPresentingInAddMealMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The MealViewController is not inside a navigation controller.")
}
}
The adding view is presented by modal, the editing view is presented by embed navigation controller, But I think this approach is not good to understand and easy maintain, How about introduce a isEditOrShow instance varible in the view to indicate the state? like following:
#IBAction func cancel(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
if isEditingOrShow = .edit{
dismiss(animated: true, completion: nil)
}
else isEditingOrShow = .show{
owningNavigationController.popViewController(animated: true)
}
}
The tutorial explains that, there are two ways in which you can dismiss view controllers.
For example, when you present a view controller as modal, you can use the below code to dismiss it.
dismiss(animated: true, completion: nil)
However, if you are using a push presentation (Navigation controller), then you should use the below code to dismiss it.
owningNavigationController.popViewController(animated: true)
How about introduce a bool isEditOrShow instance varible in the view to indicate the state?
From what I understand, you won't be needing isEditOrShow variable. If you have any questions, let me know.
I'm creating an application where I place a view controller ontop of another with
Presentation: current context
But when I'm trying to dismiss the screen by dragging the top towards the bottom the view does not disappear.
I've also tried to add a button to make it dismiss but that doesn't work either.
#IBAction func dis(_ sender: Any) {
navigationController?.popViewController(animated: true)
self.dismiss(animated: true)
}
So, how can I dismiss both views when dragging the top one down when it uses "current context" as presentation style?
I'm using "current context" because the previous screen should never be displayed again. And instead of dragging down both, I would like to drag down just one to make both of them disappear. But it does not seem to work as expected.
Although "current context" is not for this purpose as #matt mentioned,
You should dismiss the controller who presents this one to dismiss both together in this case:
self.presentingViewController!.dismiss(animated: true) // `!` is to makeing sure it's not crashing.
Demo:
Use this simple code to demo:
class ViewController: UIViewController {
#IBAction func present() {
let destination = storyboard!.instantiateInitialViewController()!
if presentingViewController != nil {
// You are not in the A
if presentingViewController?.presentingViewController != nil {
// You are in the C
presentingViewController?.presentingViewController?.dismiss(animated: true)
return
} else {
// You are in the B
destination.modalPresentationStyle = .currentContext
}
}
present(destination, animated: true, completion: nil)
}
}
Usage:
Create a single view application
Drag a button into the view controller
Replace the ViewController class with the provided code
Connect the #IBAction
Run it to see the result.
If you use fullscreen presentation (so that there is no drag-to-dismiss) it's quite simple to do this with essentially no code at all.
Note that the yellow view controller appears as an intermediary when presenting, but not when dismissing.
This question already has answers here:
Dismissing multiple modal view controllers at once?
(7 answers)
Closed 4 years ago.
I recreated the question and describe its essence more precisely.
I have two view controllers presented modally on Swift 4, without storyboard (can't use unwind) and without navigation controller
A presents B which presents C.
The reason why we don't use navigation controller, it's because we what simple animation from bottom to top and instead of breaking the standard animation from right to left, we decided to use present.
I would like to dismiss 2 view controllers and go from C to A.
Please, don't mark this question as duplicate before you read my question carefully. I found a tone of similar post, but neither solved my problem. Some of them Objective-C or some of the suggest to use:
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
Or:
self.presentingViewController?.dismiss(animated: false, completion: nil)
self.presentingViewController?.dismiss(animated: true, completion: nil)
It's works, but it create weird animation. It's just delete C and animate dismiss for B:
Expected result:
Idea: you need to dismiss third controller with animation and after dismissing you need to dismiss second without animation. While third controller is being dismissed, second shouldn't be visible.
First, set Presentation style of second view controller to Over Current Context when you're presenting it (since we will need to hide its view when we will dismiss third controller)
let vc2 = VC2()
vc2.modalPresentationStyle = .overCurrentContext
present(vc2, animated: true)
continue with creating callbacks properties for willDismiss and didDismiss inside third controller. This callback will be called before and after you dismiss third controller
class VC3: UIViewController {
var willDismiss: (() -> Void)?
var didDismiss: (() -> Void)?
#IBAction func dismissButtonPressed(_ sender: UIButton) {
willDismiss?()
dismiss(animated: true) {
self.didDismiss?()
}
}
}
then in second view controller in the place where you present third view controller, set third controller's callback properties: declare what happens when third controller will dismiss: you need to hide view of second and then after dismissing third you need to dismiss second without animation (view can stay hidden since view controller will be deinitialized)
class VC2: UIViewController {
#objc func buttonPressed(_ sender: UIButton) {
var vc3 = VC3()
vc3.willDismiss = {
self.view.isHidden = true
}
vc3.didDismiss = {
self.dismiss(animated: false)
}
present(vc3, animated: true)
}
}
Anyway, second option is using UINavigationController and then just call its method popToViewController(_:animated:).
Create a snapshot from the currently visible view and add it as a subview to the first presented view controller. To find that you can simply "loop through" the presenting view controllers and dismiss from the initial one:
#IBAction func dismissViewControllers(_ sender: UIButton) {
var initialPresentingViewController = self.presentingViewController
while let previousPresentingViewController = initialPresentingViewController?.presentingViewController {
initialPresentingViewController = previousPresentingViewController
}
if let snapshot = view.snapshotView(afterScreenUpdates: true) {
initialPresentingViewController?.presentedViewController?.view.addSubview(snapshot)
}
initialPresentingViewController?.dismiss(animated: true)
}
This is the result with slow animations enabled for the dismissal:
https://www.dropbox.com/s/tjkthftuo9kqhsg/result.mov?dl=0
If you are not using a navigation controller and you want to dismiss all ViewControllers to show the root ViewController (assuming A is the root):
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
Use this in button action method.
The current VC will dismiss when you dismiss the parent VC.
This will dismiss both VCs in single animation.
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
I have a stack of UIViewControllers like A -> B -> C. I want to go back to controller A from C. I'm doing it with below code:
DispatchQueue.global(qos: .background).sync {
// Background Thread
DispatchQueue.main.async {
self.presentingViewController?.presentingViewController?.dismiss(animated: false, completion: {
})}
}
It works but controller B seen on screen although I set animated to false. How can I dismiss two UIViewControllers without showing the middle one (B)?
P.S: I can't just directly dismiss from root controller and also I can't use UINavigationController
I searched the community but can't find anything about the animation.
Dismiss more than one view controller simultaneously
Try this.
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
Created a sample storyboard like this
The yellow view controller is type of ViewController and the button action is as follows
#IBAction func Pressed(_ sender: Any) {
self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
Output
I've created example for dismissing B controller before showing C controller. You can try it.
let bController = ViewController()
let cController = ViewController()
aController.present(bController, animated: true) {
DispatchQueue.main.asyncAfter(wallDeadline: .now()+2, execute: {
let presentingVC = bController.presentingViewController
bController.dismiss(animated: false, completion: {
presentingVC?.present(cController, animated: true, completion: nil)
})
})
}
But on my opinion solution with using navigation controller would be the best for the case. For example you can put just B controller into navigation controller -> present the navController onto A controller -> then show C inside the navController -> then dismiss from C controller whole navController -> And you will see A controller again. Think about the solution too.
Another solution
I've checked another solution.
Here extension which should solve your problem.
extension UIViewController {
func dissmissViewController(toViewController: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.dismiss(animated: flag, completion: completion)
self.view.window?.insertSubview(toViewController.view, at: 0)
dissmissAllPresentedControllers(from: toViewController)
if toViewController.presentedViewController != self {
toViewController.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
private func dissmissAllPresentedControllers(from rootController: UIViewController) {
if let controller = rootController.presentedViewController, controller != self {
controller.view.isHidden = true
dissmissAllPresentedControllers(from: controller)
}
}
}
Usage
let rootController = self.presentingViewController!.presentingViewController! //Pointer to controller which should be shown after you dismiss current controller
self.dissmissViewController(toViewController: rootController, animated: true)
// All previous controllers will be dismissed too,
// but you will not see them because I hide them and add to window of current view.
But the solution I think may not cover all your cases. And potentially there can be a problem if your controllers are not shown on whole screen, all something like that, because when I simulate that transition I don't consider the fact, so you need to fit the extension maybe to your particular case.
I was trying to implement the cancel bar button as you can see from the image, which return to the previous viewController using dismiss, but when I click the button, nothing appears, do you know why?
Here's my code to implement that:
#IBAction func cancel(_ sender: UIBarButtonItem) {
let isPresentingInAddTaskMode = presentingViewController is UINavigationController
if isPresentingInAddTaskMode {
dismiss(animated: true, completion: nil)
}
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
else {
fatalError("The AddTaskVC is not inside a navigation controller.")
}
}
Thanks in advance.
If I'm reading your question right, this is what your view controller hierarchy looks like:
For adding a new task:
Navigation Controller --(containing)--> "Managing Mode" --(modal)--> Navigation Controller --(containing)--> "Add New Task"
For editing a task:
Navigation Controller --(containing)--> "Managing Mode" --(push)--> "Add New Task"
The problem is that neither of your cases will actually work.
isPresentingInAddTaskMode
In the first case, we see this:
let isPresentingInAddTaskMode = presentingViewController is UINavigationController
if isPresentingInAddTaskMode {
dismiss(animated: true, completion: nil)
}
I'd assume this would handle the case involving the modal presentation.
This will never be true, because presentingViewController is the "Managing Mode" view controller, not the navigation controller containing it or the navigation controller containing the "Add New Task" view controller.
Another problem with this is that you have to dismiss the navigation controller containing "Add New Task", not "Add New Task" itself. This means that instead of
dismiss(animated: true, completion: nil)
you would do
navigationController?.dismiss(animated: true, completion: nil)
owningNavigationController
In the second case, we see this:
else if let owningNavigationController = navigationController{
owningNavigationController.popViewController(animated: true)
}
The first line makes sense: you're unwrapping the navigation controller. However, you then call popViewController(animated: true) on the navigation controller.
The problem with that is that both the modal and the push segues involve a navigation controller, so this case will work for both.
The Solution
You need to form a simpler cancel method using the above:
#IBAction func cancel(_ sender: UIBarButtonItem) {
guard let owningNavigationController = navigationController else {
fatalError("The AddTaskVC is not inside a navigation controller.")
}
if owningNavigationController.presentingViewController?.presentedViewController == owningNavigationController {
// modal
owningNavigationController.dismiss(animated: true, completion: nil)
} else {
// push
owningNavigationController.popViewController(animated: true)
}
}
This first unwraps the navigation controller and errors out if there is none, like what you did originally.
You then check, through a more complex way than before, if the modal segue occurred. This checks the navigation controller's presentingViewController and if it is itself. If so, it's modal and it dismisses itself. If not, it's a push segue and you pop the current view controller.