Home > Software engineering >  Remove view called by method after changing ViewController
Remove view called by method after changing ViewController

Time:01-31

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?

CodePudding user response:

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

  • use a UILabel as the "toast" view (instead of a custom view)
  • have only the slide in-and-out from the top
  • use specific y values so we don't need other undefined vars

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()
            })
        })
    })
  • Related