0

I'm strangling with this questions for some time. I read a lot about the different modalPresentationStyle, when to use each and how each one affect the view hierarchy. For example, to show some ViewController (VC2) over another ViewController (VC1) and to have a transparent background, one shall use:

    modalPresentationStyle = .overCurrentContext/.overFullScreen

Which have an opaque background by default and with assigning the background color of VC2 to clear will be opaque.

The issue is that then I lose the ViewContoller hierarchy calls. For example viewWillAppear will not be called on the presenting ViewController (VC1), and I need to use some kind of a hacky solution to notify VC1 that the above controller was dismissed.

But when I use the option that allows to utilize the ViewController hierarchy calls:

    modalPresentationStyle = .fullScreen

I loose the opaque and opacity abilities...

I know I can use delegates and basically notify them but I use Coordinators pattern which abstract the navigation and presentation away from the ViewControllers and again requires me to notify VC1 in some way (notification/called specific method) which I wonder if possible to avoid.

Pushing and using NavigaitonController does not help as well...

I'm also aware to the fact that I can use UIAdaptivePresentationControllerDelegate but again, it will require specific knowledge to be shared between coordinators that I wish not to share if possible. In addition for the fact that I dismiss the controller from the code and it will not be called

Any suggestions or API that I'm missing?

The best explanation I found is here - explain

References I have been reading through:

link-1, link-2, link-3, link-4, link-5, link-6, link-7, link-8, link-9, link-10

CloudBalancing
  • 1,461
  • 2
  • 11
  • 22

1 Answers1

0

As a general rule... viewWillAppear() is not the same things as "view will become visible again."

viewWillAppear() is part of the view controller lifecycle. Most likely, one would be executing different (or additional) code there, as opposed to when a presented controller is dismissed.

One thing you could try is to have your presenting controller conform to UIViewControllerTransitioningDelegate, and implement:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? 

Here's some example code (please note: it's Example Code Only, not "Production Ready"):

class PresentTestViewController: UIViewController, UIViewControllerTransitioningDelegate {
    
    let infoLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .yellow
        v.numberOfLines = 0
        return v
    }()
    let presentButton: UIButton = {
        let v = UIButton()
        v.setTitle("Test Present", for: [])
        v.setTitleColor(.white, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.backgroundColor = .systemRed
        return v
    }()
    
    var presentCount: Int = 0
    var dismissCount: Int = 0
    var dismissReason: String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        [infoLabel, presentButton].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
        
            // put the button at the top
            presentButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            presentButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            presentButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
            
            // put the info label below the present button
            infoLabel.topAnchor.constraint(equalTo: presentButton.bottomAnchor, constant: 20.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),

        ])
        
        presentButton.addTarget(self, action: #selector(doPresent(_:)), for: .touchUpInside)
        
        view.backgroundColor = .white
        
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // we're probably doing some view setup tasks here
        //  that we don't want to ALSO do when a
        //  presented VC is dismissed
        
        // call UI update func
        myViewWillAppear()
    }
    
    func myViewWillAppear(fromDismiss: Bool = false) -> Void {
        if !fromDismiss {
            infoLabel.text = "Info Label"
        } else {
            var str = infoLabel.text ?? ""
            str += "\n" + "Dismiss Count: \(dismissCount) Reason: \(dismissReason)"
            infoLabel.text = str
        }
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        dismissCount += 1
        if let vc = dismissed as? PresentMeViewController {
            self.dismissReason = vc.dismissReason
        }
        myViewWillAppear(fromDismiss: true)
        return nil
    }
    
    @IBAction func doPresent(_ sender: Any) {

        presentCount += 1
        var str = infoLabel.text ?? ""
        str += "\n" + "Present Count: \(presentCount)"
        infoLabel.text = str
        
        let vc = PresentMeViewController()

        vc.modalPresentationStyle = .automatic
        
        // set transitioningDelegate
        vc.transitioningDelegate = self
        present(vc, animated: true, completion: nil)

    }
    
}

class PresentMeViewController: UIViewController {
    
    private let containerView: UIView = {
        let v = UIView()
        v.backgroundColor = .white
        v.layer.borderColor = UIColor.blue.cgColor
        v.layer.borderWidth = 2
        v.layer.cornerRadius = 16
        return v
    }()
    private let stackView: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.spacing = 80
        return v
    }()
    private let testLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .green
        v.textAlignment = .center
        v.numberOfLines = 0
        v.text = "This is a label in a stack view in the view controller that will be presented."
        return v
    }()
    private let dismissButton: UIButton = {
        let v = UIButton()
        v.setTitle("Dismiss Me", for: [])
        v.setTitleColor(.white, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.backgroundColor = .systemBlue
        return v
    }()
    
    private var timer: Timer!
    
    public var dismissReason: String = ""
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        [stackView, containerView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        stackView.addArrangedSubview(testLabel)
        stackView.addArrangedSubview(dismissButton)
        
        containerView.addSubview(stackView)
        
        view.addSubview(containerView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            containerView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            containerView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            containerView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
            
            stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
            stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
            stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
            stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),

        ])
        
        // dismiss if no action after 5 seconds
        timer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(gotTimeout), userInfo: nil, repeats: false)

        // dismiss on button tap
        dismissButton.addTarget(self, action: #selector(doDismiss(_:)), for: .touchUpInside)
        
        // transparent / translucent background
        if self.presentingViewController != nil {
            view.backgroundColor = UIColor.gray.withAlphaComponent(0.25)
        } else {
            view.backgroundColor = .systemYellow
        }
        
        // this will change if Timer times-out or
        //  Dismiss button is tapped
        dismissReason = "Dragged"
    }
    
    @objc func gotTimeout() {
        dismissReason = "Timeout"
        dismiss(animated: true, completion: nil)
    }
    
    @objc func doDismiss(_ sender: Any) {
        dismissReason = "Button Tap"
        dismiss(animated: true, completion: nil)
    }
    
    // if the timer is still valid (i.e. has not "timed out")
    //  cancel the timer
    override func viewWillDisappear(_ animated: Bool) {
        if timer != nil {
            timer.invalidate()
        }
        super.viewWillDisappear(animated)
    }
    
}

The PresentTestViewController starts like this:

enter image description here

Each time we tap the "Test Present" button, our presentCount will be incremented, the "Info Label" will be updated, we'll create an instance of our PresentMeViewController, set its .transitioningDelegate, and present it:

enter image description here

If we "drag down", or tap the button, or the 5-second Timer times-out, we'll update the dismissReason var and dismiss the VC.

Back in PresentTestViewController, our implementation of animationController(forDismissed dismissed: UIViewController) will increment dismissCount, get the reason for the dismissal, and update the UI by calling myViewWillAppear(fromDismiss: true):

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thanks @DonMag, this was one approach I tried indeed with no help. The issue is that once you set `modalPresentationStyle = .fullScreen` it just does not support the ViewControler life cycle and Ill have to pass information from the PresentMeVC to the other ones... – CloudBalancing Nov 22 '21 at 14:20