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.
Related
Hello I am pretty annoyed with this:
Originally, I had many segues in my storyboard. Each button at bottom of tool bar would segue to various view controllers. As you can imagine, that is a lot of segues for 6 different items on toolbar. After segueing, each action would call self.dismiss and everything would be ok.
Recently, I wanted to clean up the storyboard.
I created the function:
extension UIViewController {
func segue(StoryboardID: String) {
let popOverVC = UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(identifier: StoryboardID)
popOverVC.modalPresentationStyle = UIModalPresentationStyle.fullScreen
popOverVC.modalTransitionStyle = .crossDissolve
self.dismiss(animated: false, completion: nil)
print("dismissed")
self.present(popOverVC, animated: false, completion: nil)
print("presented")
}
}
What I am seeing is that the dismiss dismisses the new view controller from appearing. It essentially goes back to my first view controller presented upon launch. I want to dismiss all view controllers so that I don't keep piling up my views.
Thanks
The problem is that you present your popOverVC from a view controller that is being dismissed, so your popOverVC will never be shown as its parent is being dismissed.
Not sure how your architecture is exactly but you would either need to use the delegate pattern to tell the parent when to segue, for example:
self.dismiss(animated: true) {
self.delegate?.didDismiss(viewController: self)
}
Or to use some trick to get the top most view controller to present your popOverVC after the current view has been dismissed, ex:
self.dismiss(animated: true) {
var topController: UIViewController = UIApplication.shared.windows.filter{$0.isKeyWindow}.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
topController.present(popOverVC, animated: true)
}
But I agree with #Paulw11, a UITabBarController seem like a much better option for what you're trying to do.
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 UINavigationController with several pushed view controllers.
UPD: Last pushed controller modally presents another controller.
Also, I have UINavigationControllerDelegate with some logic at navigationController:willShowViewController:animated:.
UPD: Navigation controller is its own delegate. Delegate is set in viewDidLoad method.
Question rises when I try to close all controllers programically from presented view controller:
// Close all controllers in navigation stack
presentingViewController?.navigationController?.popToRootViewController(animated: true)
// Close presented view controller
dismiss(animated: true, completion: nil)
Method navigationController:willShowViewController:animated: is not called. But it is called when I do the same without presented controller (thanks to #donmag for example project where it works).
Searched SO for answers or similar questions, but found nothing, any thoughts?
In your "presenting" VC, you want to implement a delegate/protocol pattern so your "presented" VC can call back and perform the dismiss and popToRoot...
// protocol for the presented VC to "call back" to the presenting VC
protocol dismissAndPopToRootProtocol {
func dismissAndPopToRoot(_ animated: Bool)
}
// in the presenting VC
#IBAction func presentTapped(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "presentMeVC") as? PresentMeViewController {
// Assign the delegate when instantiating and presenting the VC
vc.dapDelegate = self
present(vc, animated: true, completion: nil)
}
}
func dismissAndPopToRoot(_ animated: Bool) -> Void {
// this will dismiss the presented VC and then pop to root on the NavVC stack
dismiss(animated: animated, completion: {
self.navigationController?.popToRootViewController(animated: animated)
})
}
// in the presented VC
var dapDelegate: dismissAndPopToRootProtocol?
#IBAction func dismissTapped(_ sender: Any) {
// delegate/protocol pattern - pass true or false for dismiss/pop animation
dapDelegate?.dismissAndPopToRoot(false)
}
Here's a full demo project: https://github.com/DonMag/CustomNavController
From documentation:
popToRootViewControllerAnimated:
Pops all the view controllers on the stack except the root view controller and updates the display.
popViewControllerAnimated:
Pops the top view controller from the navigation stack and updates the display.
So seems like in order to get navigationController:willShowViewController:animated: called every time you have to do subsequent popViewControllerAnimated:, because the display got updated each time after you pop a new controller. When you pop to root view controller, update display is called only once.
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.
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.