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?
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
UILabel
as the "toast" view (instead of a custom view)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:
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":
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()
})
})
})