Dismissing to one of the previous ViewControllers and passing a value

I've got the following setup:

VC1 -> VC2 -> VC3

VC1 has a button that, when clicked, does a couple of things, then presents VC2.
VC2 has a similar button and presents VC3.
VC3 has a text field and a button. When the button is clicked, a couple of checks are run, then the text field's text should be returned to VC1.

I load the next VC with this code:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc2 = storyboard.instantiateViewController(withIdentifier: "vc2") as! ViewController2
vc2.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
vc2.someinformation = someinformation
self.present(vc2, animated: true, completion: nil)

There's no navigation controller, so I can't use popToViewController.
I also can't use an unwind segue as described here because I have to run a couple of checks before I actually dismiss the current VC and I might have to return to VC2 instead of VC1 (it all depends on the checks' result).
Some people suggest a Coordinator but that seems really complicated (and kind of overkill) for a small and pretty linear app like this.

How do I pass the value back to VC1, while also dismissing both VC3 and VC2? Is there even an easy way to go do VC1 directly, without dismissing to VC2, then using it to return the value to VC1?

Does the solution change if there are 5 VCs and I want to go from e.g. 5 to 2 or 5 to 1 (5 would be the active VC)?

Some people suggest a Coordinator but that seems really complicated (and kind of overkill) for a small and pretty linear app like this.

you could do this, and yeah it could be overkill depending on your project. After reading further though and you mentioned:

5 VCs and I want to go from e.g. 5 to 2 or 5 to 1 (5 would be the active VC)?

I would use coordinators at this point. It allows you to have whatever number VCs and also go to any VC from any VC, at any time.

Going through VC1 to VC2 to VC3 isn't too bad, but after that, it feels like a different solution is needed.
But also you don't have to do something as "heavy" as coordinators, you just need an object that lasts throughout the lifetime of all of these VCs

go to any VC from any VC, at any time.

That won't be necessary. Going forward, no VC is ever skipped, it's only going backwards that I either have to go back to VC1 (most of the time, =done) or one of the other ones (rarely, =retry).
I want to later integrate this small app into a bigger one, which pretty much requires that going back to VC1 (of this app) does not go back to the very, very first VC on the stack (= of the larger app) but instead to the first one on the "new" stack that I start with this mini-app.

you just need an object that lasts throughout the lifetime of all of these VCs

Does this mean that this object has to deal with all transitions, forth and back? Something like a struct that the VC calls when a segue is necessary? Won't there be a problem with reference circles and RAM leaks?

What's the alternative without using any kind of external class?

Edit: I can imagine that this would keep a list of all available VCs and each VC just sends it e.g. a "+1", meaning that it wants to got to the next one, or a "-2". But how would you pass information along, one VC might pass back a simple String while the other wants a list.
Edit 2: self.presentingViewController! reports the correct parent (even self.presentingViewController!.presentingViewController! does) but dismissing it just dismisses the current VC.

I tried to do the following:

protocol PassDataBackDelegate {
    func sendText(_ value:String)
    func sendTextAndSkip(_ value:String, _ skip:Bool)
}

VC3 does:

delegate.sendTextAndSkip(textViewText, true)
self.dismiss(animated: true, completion: nil)

In VC2:

func sendTextAndSkip(_ value:String, _ skip:Bool) {
    if skip {
        delegate.sendText(value)
        self.dismiss(animated: true,completion: nil)
    }
}

VC3 is dismissed but VC2 is not. Why is this not working? What do I have to do to fix this?

Edit: Looks like VC2 won't dismiss, until VC3 is dismissed and since my code always calls the delegate first, this is never the case and VC2 stays around. I moved the delegate to VC3.viewDidDisappear() and now VC2 is correctly dismissed too, however it is still displayed for a second (even with animation disabled), which isn't great. There has to be a better way to do this...

Hello, @Neph

Please refrain from posting UI related questions on Swift forums to help it stay focused on Swift the language. There is a place for this kind of questions: Apple Developer Forums.

Thank you for your cooperation. :slight_smile:

1 Like

This question is about using Swift, the language: About how to use it to pass data back and forth within my app. It's not about building the UI. There are already a bunch of other questions here on the Swift forums (even tagged "UIKit" - for the lack of a better tag) that ask similar questions.

What would be the right forum there? "Swift" is a very general place, probably better to ask that question here and the "UIKit" subforum mostly asks about designing the UI (plus, a lot of questions go unanswered there, so probably not the right place for them there either).

Just a note that since your app is linear you can always use these 2 properties of UIViewController to go up and down the navigation tree as you desire.

  • presentedViewController: "The view controller that is presented by this view controller, or one of its ancestors in the view controller hierarchy."
  • presentingViewController: "The view controller that presented this view controller."

Example method to get vc1 using presentingViewController

extension UIViewController {
    func getVc1() -> VC1? {
        return (self as? VC1) ?? presentingViewController?.getVc1()
    }
}

Example method to dismiss to a vc of certain type. Method animates the current vc dismissal while hiding those that will dismissed below without an animation. No fully tested though something like this should work.

extension UIViewController {
    func dismissToVc<T: UIViewController>(ofType type: T.Type) {
        dismiss(animated: true)
        
        var presenting = presentingViewController
        while presenting != nil && !(presenting is T) {
            presenting?.view.isHidden = true
            presenting?.dismiss(animated: false)
            presenting = presenting?.presentingViewController
        }
    }
}

Sorry about the delay!

self.presentingViewController!.dismiss() (see my edit in comment 3) only dismisses the current VC, not the parent. Calling the parent of that one (=VC2) correctly returns VC1 but it wouldn't let me dismiss it either. It looks like you can't dismiss the parent, until the current one is actually gone (so viewDidDisappear was called).
Is this a limitation of a certain Swift version?
If it did work, how would I pass a value back while going up?

I though it's also "bad" to access VCs directly like that?

from documentation of dismiss method:

The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.

If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.

So you can just find VC1 in presentingViewController chain and call dismiss on it.

For passing data you can use good old delegate pattern, used in many places in UIKit framework:

  1. create protocol defining delegate methods
  2. implement delegate methods on VC1 (or whichever type needs to respond to actions)
  3. set delegate object on VC3 or whichever type needs it
1 Like

As I said before, it doesn't work:
print(self.presentingViewController) called in VC3 does link to VC2 but if I call self.presentingViewController.dismiss() in VC3, then it only dismisses VC3, then displays VC2.

Check comment 4, I posted my delegate there but VC3 only has access to VC2's delegate (because VC2 presents VC3) and VC2 only has access to VC1's (because VC1 presents VC2). Wasn't there also a rule that you should not dismiss or access other VCs directly and that's why we're using delegates?

The presenting view controller is responsible for dismissing the view controller it presented.

If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.

If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack.

dismiss method is clearly sister to present method. As stated above, when you call VC3.dismiss method it will check whether VC3 is presenting anything, find it's not presenting anything but is presented itself, so it will call presentingViewController.dismiss which will be VC2.dismiss.

in this code when you call sendTextAndSkipVC2 is presenting VC3, so calling VC2.dismiss makes VC2 dismiss VC3. There is stack of view controllers VC1 -> VC2 -> VC3. VC2 is lower in the stack so it dismisses its child, which is also top of the stack.

Don't know what exactly you want to do in sendText and sendTextAndSkip, but if sendTextAndSkip is only there for VC2 to implement it so it can call sendText on VC1 you could just pass VC1 as delegate object to VC3, call delegate.sendText in VC3 and let VC1 react to it by dismissing its presented view controller. Let the parent dismiss child when appropriate as it is responsible for it.

As stated above, when you call VC3.dismiss method it will check whether VC3 is presenting anything, find it's not presenting anything but is presented itself, so it will call presentingViewController.dismiss which will be VC2.dismiss.

I read the quote.
I don't know what to tell you but this is not happening in my code. self.presentingViewController.dismiss() called from VC3 only dismisses VC3, so does self.presentingViewController.presentingViewController.dismiss(). Without any delegates used, simply called from within VC3.
This is what people always suggest to use to dismiss 2 VCs at the same time but it's definitely not working here. Maybe they changed the behavior within the last 5-10 years.

In this code when you call sendTextAndSkipVC2 is presenting VC3, so calling VC2.dismiss makes VC2 dismiss VC3. There is stack of view controllers VC1 -> VC2 -> VC3. VC2 is lower in the stack so it dismisses its child, which is also top of the stack.

As I said in another comment:

Looks like VC2 won't dismiss, until VC3 is dismissed and since my code always calls the delegate first, this is never the case and VC2 stays around.

Imo this is bad, unexpected behavior because when you call dismiss on a VC, then you'd expect it to dismiss itself (as the function name says) and everything it presents, not just a child (or all children down the line, idk, didn't test that).

Don't know what exactly you want to do in sendText and sendTextAndSkip, but if sendTextAndSkip is only there for VC2 to implement it so it can call sendText on VC1 you could just pass VC1 as delegate object to VC3, call delegate.sendText in VC3 and let VC1 react to it by dismissing its presented view controller. Let the parent dismiss child when appropriate as it is responsible for it.

I'm using the delegate to pass data back and, since I apparently can't make the parent dismiss itself and the child at the same time, I'm also using it to tell the parent to dismiss itself (=skip this specific VC) once the child is gon. I don't like that I have to do it separately in every VC because it requires extra checks to make sure that I end up in the right place but it's the only thing that has worked so far.

As I said in the starting post, there can be more than 3 VCs. There has to be a better way than passing VC1's delegate to VC2, then passing VC1's+VC2's delegates to VC3, VC1+VC2+VC3 to VC4,... I'm also not sure if that wouldn't create a strong reference cycle somewhere along the line.

I don't know what to tell you but dismiss works exactly as documentation says it should in my code.

protocol ThingDoerDelegate: AnyObject {
    func didAThing()
}

class ViewController: UIViewController, ThingDoerDelegate {
    override func viewDidLoad() {
        view.backgroundColor = .systemPink
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let vc = VC2()
        vc.delegate = self
        present(vc, animated: true)
    }

    func didAThing() {
        dismiss(animated: true)
    }
}

class VC2: UIViewController {
    weak var delegate: (any ThingDoerDelegate)?
    override func viewDidLoad() {
        view.backgroundColor = .systemBlue
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        let vc = VC3()
        vc.delegate = delegate
        present(vc, animated: true)
    }
}

class VC3: UIViewController {
    weak var delegate: (any ThingDoerDelegate)?
    override func viewDidLoad() {
        view.backgroundColor = .systemMint
        let button = UIButton()
        button.setTitle("Dismiss", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action: #selector(buttonPress), for: .touchDown)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }

    @objc
    private func buttonPress() {
//        dismissToVc(type: ViewController.self)
//        presentingViewController?.presentingViewController?.dismiss(animated: true)
        delegate?.didAThing()
    }
}

extension UIViewController {
    func dismissToVc<T: UIViewController>(type: T.Type) {
        var presenting = presentingViewController
        while presenting?.presentingViewController != nil && !(presenting is T) {
            presenting = presenting?.presentingViewController
        }
        presenting?.dismiss(animated: true)
    }
}

Tried it on simulators til 16.4, whichever line i leave in buttonPress method I end up with pink ViewController on screen. If you can paste it into your environment (your project or completely new project) and it works, it means there's something not as you would want it to be in your dismissing code. If this code doesn't work, my guess is you maybe found some kind of bug? Which would mean you need to upgrade your dev environment.

For a flow with different screens, each view controller having separate delegate object, it sounds like you would want coordinator to keep it organized.

I'm using Xcode 16.4 and testing it on an an iOS 18 phone. I added your code as a new "Test.swift" file and called it from my very first VC with:

let vc = ViewController()
vc.modalPresentationStyle = .overCurrentContext //Added this for all of your VCs
present(vc, animated: true)

presentingViewController?.presentingViewController?.dismiss(animated: true) indeed dismisses VC3 and VC2 and displays your pink VC. That is really weird!

Why are you using AnyObject (and any) for your delegate? Going to try marking my delegate as weak too, maybe there's some reference cycle...

For a flow with different screens, each view controller having separate delegate object, it sounds like you would want coordinator to keep it organized.

I doubt you can use the same delegate for all of them, can you?
I've now looked at two different Coordinator tutorials (e.g. this one) but they both a UINavigationController, which I do not want to use because when I'm done with this, I'm going to integrate it into a slightly bigger app, which is going to present this VC1 with overCurrentContext, no navigation controller involved.
From what I've seen, a Coordinator seems really overkill here because I haven't got multiple paths. Going forward is always going to load the VCs in order, only going back might skip a couple.

Edit:
I edited my app to basically do what yours does:
VC1 is the initial VC and it presents VC2 in an overriden viewDidAppear function (can't use viewDidLoad because it throws an "Attempt to present ... on ... whose view is not in the window hierarchy!" error). VC2 then loads VC3. VC3 displays a couple of buttons and when a specific one is clicked, then it does:

presentingViewController?.dismiss(animated: true)

This dismisses VC3 and displays VC2. "presenting.presenting.dismiss" displays VC1.
Printing the first "presenting" correctly shows a reference to VC2, while printing "presenting.presenting" references VC1, which makes sense because the VC that is presenting VC3 is VC2, which is presenting VC1 but why does dismissing the double-presenting display VC1?! It should be dismissing it (which shouldn't be possible because it's the initial VC)!
If I call "dismiss" on the VC at the very top of the stack, then that one is dismissed, as expected, but if I call "dismiss" on its parent, it only dismisses the child=the VC I accessed the parent from. This does not make any sense! "dismiss" should be working the same way, no matter where I call it from, hence dismiss the VC it's called on, not dismiss itself one time (if it's at the very top), then only dismiss its children another (if it isn't at the top)! rant end

I want to mark delegate property as weak to prevent any strong reference cycles between view controller and its delegate object, because view controller does not own the delegate object. This is quite common when you encounter this pattern, see following declarations: CBCentralManager.delegate, UICollectionView.delegate.

Weak references only make sense for reference types, like classes, so to be allowed to use that keyword in that context I need to restrict delegate protocol to class-only types - hence AnyObject.

I'm using any keyword to explicitly mark existential type, as this will be required in unspecified future Swift version. Even pre Swift 5.6, whenever you declare a variable like var a: P, where P is protocol type, you are using existential type, also called boxed protocol type, meaning variable a is of boxed type that at runtime can hold any object implementing given protocol. Some links to read on that: Protocols as Types, Upcoming Feature: existential any, SE-335 Introduce existential any.

About your observations of how dismiss works:
as you can see it works as dismiss documentation describes. dismiss is method primarily to dismiss presentedViewController - to dismiss view controller that is presented modally. If presentedViewController is nil, it tries to call presentingViewController.dismiss.

If you try calling dismiss method of view controller whose presentingViewController is nil (eg view controller that's in navigation stack of UINavigationController which itself is not presented modally) you will find that nothing will happen.

Aside from aspects of dismissing view controllers; as for passing data from one place to another you may use any of these:

  • global variables
  • same but in a form of some singleton class MyData.singleton.passedData
  • same but having that data in your appDelegate, which you could access via UIApplication.shared!.delegate as! YourViewController
  • via the delegate property in your sub view controller: if let delegate = delegate as! YourClass? { v.passedData = ... }
  • similar idea but instead passing some action callback to the child controller, which action will be called with data when that's available.
  • similar idea but instead passing SwiftUI's Bindings to UIKit controllers (heh).
  • via notification centre (making observation in one place, passing notification with your data attached in another place)
  • writing data in user defaults in one place and reading it from there in another place (especially if that's useful for other purposes)
  • segue.destination as! YourViewController
  • instead of relying on your delegate fields use presenting / presented View controller fields directly
  • similar idea but via view controller's parent link

Don't spend too much on this, just pick one that works. The days of UIKit are pretty much numbered so it doesn't make much sense making this perfect.