in my iOS app the user navigates using a navigation controller.
The user navigates using different UITableViewController.
UIViewController - UITableViewController - UITableViewController
After the user has selected an item in the "last" UITableViewController I am using the following call to return to the root view controller:
DispatchQueue.main.async { self.navigationController?.popToRootViewController(animated: true) }
How can I send data back to the root controller? Previously I used notifications, but I really dislike this concept. Is there a more elegant way?
Should every single UIViewController on the view-stack implement his own callback which is initialized using the
func prepare(for segue: UIStoryboardSegue, sender: Any?)
method?
How can this be implemented elegantly?
If you are using segues, you could create an unwind segue and use it instead of popToRootViewController. Here you can see how to create an unwind segue.
Otherwise, based on your hierarchy, before popToRootViewController, you could access your view controller like this:
if let rootVC = navigationController?.viewControllers.first as? YourViewControllerClass {
rootVC.someProperty = dataToPass
}
navigationController?.popToRootViewController(animated: true)
Related
My app has a navigation controller where the first view controller is a login screen and all other screens are pushed on top.
I would like to unwind to the login screen whenever any http request to the backend returns a 401 error.
What I had in mind was to add an extension to the ViewController class with something like this:
extension UIViewController {
func unwindToLoginScreen() {
performSegue(withIdentifier: loginScreen)
}
}
And the segue would be an unwind segue. Then, whenever the request fails I call the view controller's unwindToLoginScreen method.
However, the problem with this approach is that I would have to remember to create said unwind segues on the storyboard for all new view controllers that I added to the project.
So far I think the ideal plan would be to be able to create the unwind segue programatically instead of using the storyboard. So, my unwindToLoginScreen() extension method would work in any new view controller by default. Something like:
extension UIViewController {
func unwindToLoginScreen() {
let segue = UnwindSegue(identifier: "blablah", segue: LoginViewController.unwindToLoginViewController)
segue.perform()
}
}
Is it possible to do something like this?
You can't create segues in code, but you can pop to the root of the UINavigationController's stack:
func returnToLoginScreen() {
self.navigationController?.popToRootViewController(animated: true)
}
If you want to pop to a viewController that isn't the root, you can find it in the array of viewController's managed by the UINavigationController and then call popToViewController:
// pop to second viewController in the stack
self.navigationController?.popToViewController(self.navigationController!.viewControllers[1], animated: true)
... or search for the ViewController by type:
if let loginVC = self.navigationController?.viewControllers.first(where: { $0 is LoginViewController }) {
self.navigationController?.popToViewController(loginVC, animated: true)
}
i am a beginner in iOS development, and recently, i just follow along the tutorial for beginners.
let say i want to move from one VC to another VC by clicking a button, so i just find out that there are three ways to move from one ViewController to another ViewController (modal segue).
in main storyboard, i just click control and drag from the button to th destination view controller and choose present modally
programmaticaly, by implementing the code below
#IBAction func logInButtonDidPressed(_ sender: Any) {
// modal transition to VC2
let viewController2 =
storyboard?.instantiateViewController(withIdentifier:
"ViewController2") as! ViewController2
present(viewController2, animated: true, completion: nil)
}
programatically,by using perform segue function
#IBAction func logInButtonDidPressed(_ sender: Any) {
performSegue(withIdentifier: "toSecondViewController", sender: self)
}
are they just the same ? or is it used for different cases?
Thanks in advance :)
Yes, they are similar. And the obvious difference I think is the data passing. The first and third one are same, use the following method to pass data to next controller:
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if let viewController2 = segue.destination as? ViewController2 {
viewController2.someProperty = someValue
}
}
For second transition, you directly set the data when creating the next controller:
let viewController2 = storyboard?.instantiateViewController(withIdentifier:
"ViewController2") as! ViewController2
viewController2.someProperty = someValue
present(viewController2, animated: true, completion: nil)
I would use segues, as there are some advantages compared to manual presentation:
You can create unwind segues to exit the current view controller to any view controller in the hierarchy.
You can add 3D touch support to segues with one mouse click.
The first and last method produce identical results. I would create segues with clicking and dragging whenever possible. If you need to do some data validation or other stuff before performing a transition, you have to call the performSegue method manually.
I start of with a tableViewController that has a list of names. When the user taps on a name, they are segued to a view controller.
While in that viewController the user may press a button that will take them to another table view Controller.
The layout is like this:
TableViewController(1) -> ViewController -> TableViewController(2)
My question is, how can I pop back to the first TableViewController from the Second TableViewController.
My rootViewController is my signIn View controller so I cannot pop back to root.
You can run this to pop to your rootViewController:
self.navigationController?.popToRootViewController(animated: true)
Update:
Since your rootViewController is not where you want to end up then you can iterate through your controllers and pop to a specific one:
for controller in self.navigationController!.viewControllers {
if controller.isKind(of: TableViewControllerOne.self) {
self.navigationController!.popToViewController(controller, animated: true)
break
}
}
Instead of TableViewControllerOne.self update to your desired controller.
If you're familiar with segues, you can implement an unwind segue. That would give you the added benefit of passing information back to TableViewController(1) if you needed to. To make that work in TableViewController1 you would add some code that looked like:
#IBAction func unwind(fromTableVC2 segue: UIStoryboardSegue) {
if (segue.source is TableVC2) {
if let svc = segue.source as? TableVC2 {
// pass information back
}
}
}
Then in your storyboard you would go to where you have your TableVC2 and drag the yellow VC circle to the exit and choose the function we created above. Name the segue (for this example we'll call it "UnwindToTableVC1"), and then somewhere in TableVC2 add the code:
func setVariableToPassBack () {
// Set up variables you want to pass back
performSegue(withIdentifier: "UnwindToTableVC1", sender: self) }
And that will take you back to your chosen destination with any information you wanted to pass back.
If you don't want to pass anything back, you really just need call the below line in your TableVC2:
performSegue(withIdentifier: "UnwindToTableVC1", sender: self)
I have a sent Action, as follows:
#IBAction func showSettings(sender: AnyObject) {
let settingsPicker = SettingsViewController()
settingsPicker.setDelegate(self)
let navigationController = UINavigationController (rootViewController: settingsPicker)
self.presentViewController(navigationController, animated: true, completion: nil)
}
The method creates a controller, sets a reference to the delegate, and creates a navigation controller.
All this works, however the widgets defined in the story board do not appear. The SettingsViewController should manage a ui which is defined in a story board. I presume becuase I create it programmatically none of the widgets appear. The SettingsViewController does not programmatically create widgets, the are declaratively defined in the story board.
If I link (in the storyboard) the two controllers with a segue, then the widgets appear, but my action is not being used.
How can I use my action and present the view controller / ui as defined in the storyboard?
When you create a segue between your UIViewControllers, you should define an identifier, eg: "settingsSegue".
In your code you can then perform that segue by calling the segue with the identifier:
#IBAction func showSetting(sender: AnyObject) {
performSegueWithIdentifier("settingsSegue", sender: nil)
}
To set up the SettingsViewController you should implement the following:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?){
if let settingsController = segue.destinationViewController as? SettingsViewController {
settingsController.delegate = self
}
}
Interacting with Storyboard and Segues
If you want to invoke a segue through code, see Laffen's answer.
If you want to create a view controller that's defined in your storyboard and then display it programmatically, use instantiateViewControllerWithIdentifier to create a new instance of your view controller, then display it to the screen as desired (present it modally, push it onto your navigation stack, or whatever you want to do.)
I have two UICollectionViewControllers and the first one uses a push segue to get to the second one. The problem I'm having is passing information back to the first controller when the back button (the one that gets added automagically) is pressed in the second controller. I've tried using the segueForUnwindingToViewController, and canPerformUnwindSegueAction override functions, but no dice. I need to be able to access both view controllers so I can set some variables. Any ideas?
Here is an example with two view controllers. Let's say that the names of the two view controllers and ViewController and SecondViewController. Let's also say that there is an unwind segue from the SecondViewController to the ViewController. We will pass data from the SecondViewController to the ViewController. First, let's set the identifier of this segue by opening the document outline and selecting the unwind segue. Then open up the attributes inspector and set the identifier to "unwind".
SecondViewController Code:
class SecondViewController: UIViewController
{
override func prepareForSegue(segue: UIStoryBoardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
if let destination = segue.destinationViewController as? ViewController {
if identifier == "unwind" {
destination.string = "We Just Passed Data"
}
}
}
}
}
ViewController Code:
class ViewController: UIViewController {
var string = "The String That Will Be We Just Passed Data"
#IBAction func unwindSegue(segue: UIStoryBoardSegue) {
}
}
It sounds like you are trying to intercept the back button, there are many posts for this on SO, here are two:
Setting action for back button in navigation controller
Trying to handle "back" navigation button action in iOS
In practice, it is more clear to return state in closures (more modern), or delegates.