In my Xcode-App, a modal can be opened from every view. Every 'base' view is having a different purpose, some are showing a table, some are doing not. How can I achieve to reload the 'base' view whenever the modal is dismissed?
It seems to be especially tricky as the views have such different structures and purposes. I tried viewWillAppear, viewDidAppear and viewDidLoad, but none of them seem to do the trick.
You can setup a delegate pattern so that your modal view can notify when it will or did disappear.
First you need to create a protocol for your delegate:
protocol ModalViewControllerDelegate: class {
func modalControllerWillDisapear(_ modal: ModalViewController)
}
Then your modal should have a delegate property (that will in the end be the presenting controller) and trigger the modalControllerWillDisapear method when needed:
final class ModalViewController: UIViewController {
weak var delegate: ModalViewControllerDelegate?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.modalControllerWillDisapear(self)
}
}
And all view controller that will present your modal controller must conform to that protocol and assign itself as a delegate of the modal when presenting:
final class SomeViewController: UIViewController {
private func presentModalController() {
let modal = ModalViewController()
modal.delegate = self
self.present(modal, animated: true)
}
}
extension SomeViewController: ModalViewControllerDelegate {
func modalControllerWillDisapear(_ modal: ModalViewController) {
// This is called when your modal will disappear. You can reload your data.
print("reload")
}
}
Note: If you are using segues to present your modal, you can assign the delegate property in the prepare(for:sender:) method instead of in a custom method.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch (segue.identifier, segue.destination) {
// Check that the segue identifer matches and destination controller is a ModalViewController
case ("showModalSegue", let destination as ModalViewController):
destination.delegate = self
case _:
break
}
}
Related
i am calling a GET(method) API on viewDidAppear function of a view controller. i am presenting a new view controller using navigation controller over my first view controller. on the second view controller i am calling an API of Post Method to add another entry into my previous screen Get method API. But when I dismiss the second View Controller the Get API data remains the same and when i again runs the code the data was updated on the first view controller. Can someone tell me that how to check on first view controller that my second view controller is dismissed so that i can call API there.
I got the solution for this. It didn't work by calling the API on viewDidAppear() or viewWIllAppear() . This will be done by using swift closures.
Below is the code:
class 1stViewController: UIViewController {
#IBAction func buttonTapped(_ sender: UIButton) {
guard let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else { return }
secondController.callbackClosure = { [weak self] in
print("call API")
}
self.navigationController?.pushViewController(secondController, animated: true)
}
}
On Second view Controller:
class SecondViewController: UIViewController {
var callbackClosure: ((Void) -> Void)?
override func viewWillDisappear(_ animated: Bool) {
callbackClosure?()
}
}
I got a little problem.
On my main view controller I got a bar button that opens a slide menu, which is a regular view controller using a slide in transition. The slide menu has a button to open another view controller. When the new view controller is opened, you have the option to cancel, which dismisses the current view controller. The problem is, that the user ends up in the menu view once again, instead of the main view controller. Would be very happy to know what I am doing wrong :)
func openSupport() {
guard let creditViewContoller = storyboard?.instantiateViewController(withIdentifier: "support") as? CreditViewController else { return }
present(creditViewContoller, animated: true)
}
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController()
menuView.dismiss(animated: true, completion: nil)
openSupport()
print("Tap on Support")
}
you can dismiss view controller simply by using
self.dismiss(animated: true, completion: nil)
Consider
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController() // (1)
menuView.dismiss(animated: true, completion: nil) // (2)
openSupport() // (3)
print("Tap on Support")
}
This:
Creates new MenuViewController but never presents it;
Calls dismiss on view controller that was never presented; and
Calls openSupport from this MenuViewController instance (which was never dismissed).
Bottom line, you want to let the main view controller that presented the menu do the presenting. So, the menu view controller should:
Define a protocol for it to inform the presenting view controller to transition to the next scene:
protocol MenuViewControllerDelegate: class {
func menu(_ menu: MenuViewController, present viewController: UIViewController)
}
And then the menu view controller can, when it’s done dismissing, tell its delegate what it should present:
class MenuViewController: UIViewController {
weak var delegate: MenuViewControllerDelegate?
#IBAction func didTapSupport(_ sender: Any) {
dismiss(animated: true) {
guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "support") else { return }
self.delegate?.menu(self, present: controller)
}
}
#IBAction func didTapCancel(_ sender: Any) {
dismiss(animated: true)
}
}
Then the main view controller needs to
Make sure to set the delegate of the menu view controller:
class ViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? MenuViewController {
destination.delegate = self
}
}
}
and
Make sure to present the view controller that the menu controller asked it to:
extension ViewController: MenuViewControllerDelegate {
func menu(_ menu: MenuViewController, present viewController: UIViewController) {
present(viewController, animated: true)
}
}
There are lots of different ways of achieving this, so don’t get lost in the details here. But the idea is to have some system by which the menu view controller can request whomever is to present the support view to do so, not try to do it itself.
I have a splitview controller and the Master view has a button (Note) that pushes the Note view onto the Detail view. The Detail view makes used of navigation controller to help users navigate back and forth among multiple view controllers. Inside those view controllers, I have delegate methods that pushes Note view onto itself. This is what my UI look like :
The Note button works as I expected when the app is initially run. It still works when I tap one of the list elments and traverse to the views at the deeper level. However, it stops working when I go back to the very first view (which was working initially). I'm not sure what is causing this inconsistent behaviour and I appreciate much if you guys could help me figure this out.
This is excerpt of my code :
Master View
protocol ChildViewDelegate: class {
func updateView()
func pushOntoDetailViewNaviController(_ viewName: String)
}
class MasterViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate, GADBannerViewDelegate {
....
#IBAction func noteTapped(_ sender: UIBarButtonItem) {
pushOntoActiveNaviController("NoteGalleryView")
}
private func pushOntoActiveNaviController(_ viewName: String) {
guard let splitView = self.splitViewController else {
return
}
if splitView.viewControllers.count > 1 {
// Push view onto any active detailed view
self.delegate?.pushOntoDetailViewNaviController(viewName)
} else { //If active view is the master view (true for iphone), then push it onto the master view
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
....
}
Detail View
class DetailTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, GADBannerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
masterViewController = (self.splitViewController?.viewControllers.first as! UINavigationController).topViewController as? MasterViewController
masterViewController?.delegate = self
}
extension DetailTableViewController: ChildViewDelegate {
func updateUI() {
....
}
func updateView() {
DispatchQueue.main.async {
self.tableView.reloadData()
self.updateUI()
}
}
func pushOntoDetailViewNaviController(_ viewName: String) {
if let vc = storyboard?.instantiateViewController(withIdentifier: viewName) {
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
In case somebody has the same issue, it's because I'm setting delegate variable inside viewDidLoad which is invoked when view is initially loaded. It isn't invoked when view appears again when user navigates back to the view from deeper view controllers. The delegate variable should have been set inside viewWillAppear.
I'm trying to perform a segue from ViewController A to ViewController A (same VC). I can do that when I ctrl+drag from a button to the VC and set the identifier ("sameVC" in my case).
When the button is tapped, the segue works as expected. The problem is, I need to call
self.performSegue(withIdentifier: "sameVC", sender: self)
from a delegate. So when the button is tapped, I need to call a function, that has a delegate, like this:
class ViewControllerA: ViewController, MyDelegate {
var mC = MyClass()
override func viewDidLoad() {
mC.delegate = self
}
#IBAction func buttonTapped(_ sender: UIButton) {
mC.myfunc()
}
func success {
// Trying to call delegate
self.performSegue(withIdentifies: "sameVC", sender: self)
}
}
protocol MyDelegate {
func success()
}
class MyClass {
var delegate: MyDelegate?
func myfunc() {
// ...
self.delegate.success()
}
}
When the delegate comes back to ViewControllerA in "success"-function, I want to perform the segue, but the View isn't present anymore, because the segue was called implicit with "buttonTapped"-function.
When the segue isn't going to the same VC, I would just ctrl+drag from ViewControllerA to ViewControllerB, but this isn't possible with only one ViewController (or I don't know how to do this).
Is there any way I can achieve this?
Instead of ctrl+dragging from button, ctrl+drag from ViewController itself, then it will wait for you to call performSegue
And for going from you have more than one viewcontrollers you may want to navigate to, you can assign an Identifier to your viewControllers in your storyboard and use storyboard.instantiateViewControllerWithIdentifier method to instantiate your desired ViewController and present it to the user, e.g. push it on top of current viewcontroller.
I have a UITableViewController that, when presented from one screen, is presented with the standard 'show' segue from right to left, and when presented from another screen (a UIViewController), is presented modally from the bottom. I got this to work properly with the help I got from a question I asked a few months ago (has screenshots).
The key was creating the segue from the UINavigationController of my Settings screen to the shared UITableViewController instead of creating it from the UITableViewCell. Strangely though, even though it presents correctly from right to left, dismissing it closes it modally (top to bottom).
I'm making the presenting table view controller a delegate of the UITableViewController it's presenting so it will handle the dismissal. Here's the protocol and extension it implements (Swift 2.3):
protocol DismissalDelegate : class {
func selectionDidFinish(controller: UIViewController)
}
extension DismissalDelegate where Self: UIViewController {
func selectionDidFinish(viewController: UIViewController) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
And I set it in the segue defined in the presenting controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "goToLifts" {
let destination = segue.destinationViewController as! LiftSelectionTableViewController
destination.dismissalDelegate = self
} else {
return
}
}
The presented table view controller calls delegate?.selectionDidFinish(self) when the user makes a selection (in didSelectRowAtIndexPath):
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
guard (dismissalDelegate != nil ) else {
return
}
dismissalDelegate?.selectionDidFinish(self)
}
That calls this method in the presenting table view controller:
func selectionDidFinish(controller: LiftSelectionTableViewController) {
self.dismissViewControllerAnimated(true, completion: nil)
}
I've looked through the APIs for presenting view controllers and haven't been able to find anything that exposes options to control this. The dismiss(animated:completion:) API even says it's for dismissing a view controller presented modally, but I don't see anything else having to do with dismissal.
How can I get this thing to dismiss the same way it's presented when it's presented from my UITableViewController (right to left, and back) but keep the modal behavior when presented from the other view (a UIViewController)?
I'm a little confused here, it looks like you are using the delegate because the presenting view controller should know how LiftSelectionTableViewController got presented.
So in the table view controller, you would have
func selectionDidFinish(viewController: UIViewController) {
self.navigationController?.popViewControllerAnimated(true)
}
In the other view controller, you should have
func selectionDidFinish(viewController: UIViewController) {
self.dismissViewControllerAnimated(true, completion: nil)
}
If I'm wrong and you can't know how the view controller was presented, then I would try checking to see if the top view controller on the navigation controller is presented view controller. Pop the view controller if it is, dismiss the view controller if it isn't.
func selectionDidFinish(viewController: UIViewController) {
if self.navigationController?.topViewController == viewController {
self.navigationController?.popViewControllerAnimated(true)
} else {
self.dismissViewControllerAnimated(true, completion: nil)
}
}