iosswiftviewviewcontroller

Remove view called by method after changing ViewController


I have an extension ViewController for showing toast view. It should be added to parent view, then slide from top or bottom of screen and after delay slide back out of screen and be removed from parent view.

extension UIViewController {
func showInfoToastView(text: String, image: UIImage?, duration: Double, position: ToastPosition) {
    ...
    let toast = ToastView(viewModel: viewModel, frame: frame)
    self.view.addSubview(toast)
    
    slideToastView(toast: toast, position: position, duration: duration)
}

func showActionToastView(text: String, image: UIImage?, duration: Double, position: ToastPosition, action: Handler?) {
    ...
    let toast = ToastView(viewModel: viewModel, frame: frame)
    self.view.addSubview(toast)
    slideToastView(toast: toast, position: position, duration: duration)
    
}
private func slideToastView(toast: ToastView, position: ToastPosition, duration: Double) {
    let width = UIScreen.main.bounds.width / 1.2
    let barHeight = self.topbarHeight
    switch position {
    case .top:
        toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2,
                                y: -70 - barHeight,
                                width: width,
                                height: 60)
         
        UIView.animate(withDuration: 0.5, animations:  {
            toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2,
                                 y: 70 - barHeight,
                                    width: width,
                                    height: 60)
        }, completion: { done in
            if done {
                print(self.topbarHeight)
                DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: {
                    UIView.animate(withDuration: 0.5, animations:  {
                        toast.frame = CGRect(x: (UIScreen.main.bounds.width - width)/2,
                                             y: -70 - barHeight,
                                                width: width,
                                                height: 60)
                    }, completion: { finished in
                        if finished {
                            toast.removeFromSuperview()
                        }
                    })
                })
            }
        })
    case .bottom:
        toast.frame = CGRect(x: (view.frame.size.width - width)/2,
                             y: UIScreen.main.bounds.size.height,
                                width: width,
                                height: 60)
         
        UIView.animate(withDuration: 0.5, animations:  {
            toast.frame = CGRect(x: (self.view.frame.size.width - width)/2,
                                 y: UIScreen.main.bounds.size.height - 200 - self.tabBarHeight,
                                    width: width,
                                    height: 60)
        }, completion: { done in
            if done {
                DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: {
                    UIView.animate(withDuration: 0.5, animations:  {
                        toast.frame = CGRect(x: (self.view.frame.size.width - width)/2,
                                             y: UIScreen.main.bounds.size.height,
                                                width: width,
                                                height: 60)
                    }, completion: { finished in
                        if finished {
                            toast.removeFromSuperview()
                        }
                    })
                })
            }
        })
    }
}

}

But when I call this method, quickly switch to another controller and then back, the view does not disappear and remains at the top of the screen. How do I make all the views caused by this method disappear when switching the controller?


Solution

  • First, a tip: It is much easier to get help if you provide a complete minimal example. Your code cannot be run as-is because you only posted snippets.

    Here's a better example...

    In this minimal code, we

    Extension

    extension UIViewController {
        
        func showInfoToastView(text: String) {
            // create a Yellow Label as the "toast view"
            let toast = UILabel()
            toast.backgroundColor = .yellow
            toast.textAlignment = .center
            toast.text = text
            
            self.view.addSubview(toast)
            
            slideToastView(toast: toast)
        }
    
        private func slideToastView(toast: UIView) {
            
            let width = UIScreen.main.bounds.width / 1.2
    
            toast.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2.0,
                                 y: -70.0,
                                 width: width,
                                 height: 60.0)
            
            UIView.animate(withDuration: 2.0, animations:  {
                toast.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2.0,
                                     y: 70.0,
                                     width: width,
                                     height: 60.0)
            }, completion: { done in
                if done {
                    print("animate into view completion block")
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: {
                        UIView.animate(withDuration: 0.5, animations:  {
                            toast.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2.0,
                                                 y: -70.0,
                                                 width: width,
                                                 height: 60.0)
                        }, completion: { finished in
                            if finished {
                                print("animate out of view completion block")
                                toast.removeFromSuperview()
                            }
                        })
                    })
                }
            })
    
        }
    }
    

    And an example view controller with two buttons - one to show the "toast" and one to push to a new controller.

    Example View Controller

    class ViewController: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            if self.navigationController == nil {
                print("Must be in a Navigation Controller!")
                return
            }
            
            var config = UIButton.Configuration.filled()
            
            config.title = "Show Toast"
            
            let b1 = UIButton(configuration: config, primaryAction: UIAction() { _ in
                self.showInfoToastView(text: "This is a test.")
            })
    
            config.title = "Push to next VC"
            
            let b2 = UIButton(configuration: config, primaryAction: UIAction() { _ in
                // push to an empty systemYellow view controller
                let vc = UIViewController()
                vc.view.backgroundColor = .systemYellow
                self.navigationController?.pushViewController(vc, animated: true)
            })
            
            [b1, b2].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                b1.topAnchor.constraint(equalTo: g.topAnchor, constant: 80.0),
                b1.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                b2.topAnchor.constraint(equalTo: b1.bottomAnchor, constant: 40.0),
                b2.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                b2.widthAnchor.constraint(equalTo: b1.widthAnchor),
            ])
            
        }
        
    }
    

    If you assign ViewController as the class of an empty UIViewController and make it the root controller of a UINavigationController, you can run this without any edits - and it will look like this:

    enter image description here enter image description here

    Your description of the problem - "when I call this method, quickly switch to another controller and then back" - wasn't quite clear, but I'm guessing you meant "if I navigate to a new controller before the animation is finished." So, we've also slowed down the "animate-in" to 2-seconds to make it easier to tap the "push" button during the animation.

    If that's the case, the problem is your use of "is animation done":

    enter image description here

    If we navigate away before the animation finishes, nothing in the gray box will execute! You can confirm this by seeing that nothing from either of the print() statements shows up in the debug console.

    Remove that "done" test (the "finished" test is not needed either), and run this example again. You'll see that the "toast" view will either animate away when you come back, or will already be gone if you come back after the 3-second delay.

        UIView.animate(withDuration: 2.0, animations:  {
            toast.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2.0,
                                 y: 70.0,
                                 width: width,
                                 height: 60.0)
        }, completion: { done in
            // no need to test for done
            print("animate into view completion block")
            DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: {
                UIView.animate(withDuration: 0.5, animations:  {
                    toast.frame = CGRect(x: (UIScreen.main.bounds.width - width) / 2.0,
                                         y: -70.0,
                                         width: width,
                                         height: 60.0)
                }, completion: { finished in
                    // no need to test for finished
                    print("animate out of view completion block")
                    toast.removeFromSuperview()
                })
            })
        })