Every controller takes care of a visual component on the screen. Some controllers are responsible for the whole screen estate; some take care of only a part of it.
The key concept in presenting new screens is a segue. This is a transition without interruption from one view controller to another. Using the segues, we can link together different scenes in our app and we can even transfer information between view controllers. Each segue can define different animations when transiting from once scene to another.
There are four main kinds of segues, each with its own unique approach and attributes, and which act differently depending on the size class they're in, or whether they're embedded in a navigation controller or a split view controller.
So, each segue is a relation (transition) between different screens in your app, and those transitions can be fired upon a user's action or by using any other trigger (time trigger, or an action from a server):
This link represents a relation between those two screens. If we add another segue, which will lead to another view controller, then we will have another link between these view controllers.
You can select the segue and add an identifier in the properties panel on the right. This identifier can be used to trigger a segue with code. For example, the selected segue has the following ID - showSecond
Another way to activate a segue is to use code and then add an action that will be fired upon using the Touch Up Inside event. Then in the function, you can activate the segue with the following code:
@IBAction func onDatailsClicked(_ sender: Any) { performSegue(withIdentifier: "showSecond", sender: sender) }
You should create a segue with the showSecond
identifier on the storyboard.
Now, we know how to transition from one scene to another, but we should pass data and make the user believe that those scenes are related.
Passing data forward
Passing Data Between View Controllers With Properties. A property is a variable that’s part of a class. Every instance of that class will have that property, and you can assign a value to it.
Here’s a view controller MainViewController with a property called text:
class MainViewController: UIViewController { var text:String = "" override func viewDidLoad() { super.viewDidLoad() } }
Let’s say this view controller is part of a navigation controller and we want to pass some data to a second view controller. Here’s the actual passing of the data. Add the following method to MainViewController:
@IBAction func onButtonTap() { let vc = SecondaryViewController(nibName: "SecondaryViewController", bundle: nil) vc.text = "Hello there!" navigationController?.pushViewController(vc, animated: true) }
Passing Data Between View Controllers Using Segues. Before you can pass data between two view controllers, you need to connect them with a segue. Then you need to make sure that each view controller has its own .swift file that you can connect it to through the Identity Inspector. Finally, you need to write Swift code in the .swift files of both view controllers to send and receive data.
First, when designing each screen, we make it dependent on some model (data). This model should be passed when the transition happens and once the view controller is presented, the data should appear. This way, the user thinks that the two screens are linked together. An example is a collection view and the details view, which displays extra info. These two screens can work independently, but when we pass data from one to the other, they are perceived as the same thing.
Now, let's open the code and jump straight to the view controller that we want to send data, when a specific segue is fired. In that controller, we have to override the following method:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let id = segue.identifier { switch id { case "showDetails": print("transfer the data"); default: break; } } }
In the preceding code, we detect which segue will be performed. We know which screen will be next. The new view controller that will be presented on the screen should accept the data. Usually, the view controller has a public
property (properties) which should be set. You have two arguments. The first one is the segue
object, which contains both view controllers. The sender
object identifies the item that has triggered the segue. Both arguments are needed to distinguish between different logical scenarios and we can develop the app to act differently in different cases. Here is what the code should look like if we want to pass some data to the second view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let id = segue.identifier { switch id { case "showDetails": guard let secondVC: SecondViewController = segue.destination as? SecondViewController else { return } secondVC.receivedData = 42 default: break; } } }
or
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let secondVC = segue.destination as? SecondViewController { secondVC.receivedData = myTextField.text ?? 42 } }
or
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { case "showItem": if let row = tableView.indexPathForSelectedRow?.row { let item = items[row] let detailViewController = segue.destination as! DetailViewController detailViewController.item = item } default: preconditionFailure("Unexpected segue identifier.") } }
The UIStoryboardSegue
gives you three pieces of information: the source view controller (where the segue originates), the destination view controller (where the segue ends), and the identifier of the segue. The identifier lets you differentiate segues.
The receivedData
property can be used freely in the viewDidLoad()
method. We can pass a lot of information, which can be used in the next view controller. There is no limitation as to what type it should be. When the next view controller is activated, it will have access to the passed bits.
There’s another way to pass data through a segue that involves giving the segue a name. Then your code can run depending on the segue name. This method can be handy in case you have multiple segues linked to the same view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let secondVC = segue.destination as? SecondViewController if segue.identifier == "oneSegue" { secondVC?.receivedData = myTextField.text ?? 42 } if segue.identifier == "twoSegue" { secondVC?.receivedData = 44 } }
A slightly different problem is to pass data in the reverse direction. Let's try to understand why we need it first. When we do some actions in a child view controller, its nice to pass the data to the parent view controller. This approach will improve the module's design. Every view controller does it's own job and passes the model after some updates.
Passing data in the reverse direction
Using protocol. To use the delegation pattern, you’ll need to set up a delegate protocol that defines a list of all the methods that the delegate should implement. The first view controller would then adopt the protocol and define itself as the second view controller’s delegate.
Create the delegate protocol. The naming convention for the delegate of a class is to use the same name of the class with the suffix Delegate
. Add the SecondViewControllerDelegate
protocol to the SecondViewController.swift file.
protocol SecondViewControllerDelegate { func saveMovie(_ move: Movie) }
Add a reference to the delegate in SecondViewController, and make it an optional.
var delegate: SecondViewControllerDelegate?
Now, to extract the data that the user has entered in the form, you’ll need to create outlets for each of the elements in the form.
In the touchSave
method (SecondViewController) before calling the dismissMe
method, create a object from the fields in the edit form, and pass it into the delegate method:
@IBAction func closeButton(_ sender: UIButton) { let movieToSave = Movie( title: titleTextField.text!, rating: 7 ) delegate?.saveMovie(movieToSave) dismiss(animated: true, completion: nil) }
During the segue, the FirstViewController class needs to tell the SecondViewController that it is the SecondViewController’s delegate. The problem is that because the segue was created in Interface Builder, the instantiation of the new view controller is managed automatically.
Fortunately, view controllers contain a prepareForSegue
method that’s called after any new view controllers are instantiated but before the segue is performed.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let navController = segue.destination as? UINavigationController { if let secondViewController = navController.topViewController as? SecondViewController { secondViewController.delegate = self } } } ... extension FirstViewController: SecondViewControllerDelegate { func saveMovie(_ move: Movie) { ... } }
Using delegate. Another way to pass data backward is to declare the first view controller as a delegate. Then define properties in both view controllers that hold the data you want to pass back from the second view controller.
class SecondViewController: UIViewController { @IBOutlet var myTextField: UITextField! var sentText : String = "" var delegate : FirstViewController! override func viewDidLoad() { super.viewDidLoad() } @IBAction func closeButton(_ sender: UIButton) { sentText = myTextField.text ?? "default value" delegate.receivedText = sentText dismiss(animated: true, completion: nil) } } class FirstViewController: UIViewController { @IBOutlet var myLabel: UILabel! var receivedText : String = "" override func viewDidLoad() { super.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { myLabel.text = receivedText } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let vc = segue.destination as! SecondViewController vc.sentText = self.receivedText vc.delegate = self } }
Using Notification Center. Yet another way to pass data between view controller files is through the notification center. Using the notification center to pass data can be especially useful when you need to share data between two or more view controllers at the same time, or if the view controllers are not connected by a segue. There’s a three-step process to using notification center:
To define a name for a notification center, you can choose any arbitrary name such as
static let notificationName = Notification.Name("myNotification")
To add a notification center observer involves defining a function to run when it receives a notification and defining the name of the notification center to observe. This can be done with a statement like this:
NotificationCenter.default.addObserver(self, selector: #selector(functionName(notification:)), name: notificationName, object: nil)
Where functionName
is the name of your function to run when the notification is received, and notificationName
is the name of the notification center you defined.
Finally, you need to send a notification and pass data at the same time. This can be done using this statement:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "Notification Name"), object: dataSent)
Where "Notification Name" is the name you chose for the notification center, and dataSent
is any data you wish to pass to a notification observer.
class FirstViewController: UIViewController { @IBOutlet var myLabel: UILabel! static let notificationName = Notification. Name("myNotification") @objc func onNotification(notification:Notification) { let data = notification.object let temp = String(describing: data!) myLabel.text = temp } override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: FirstViewController.notificationName, object: nil) } } class SecondViewController: UIViewController { @IBOutlet var myTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() } @IBAction func closeButton(_ sender: UIButton) { let dataSent = myTextField.text NotificationCenter.default.post(name: NSNotification.Name(rawValue: "myNotification"), object: dataSent) dismiss(animated: true, completion: nil) } }
Pass in a closure. This alternative has similarities to the delegation pattern, but focuses on one closure rather than a list of methods in a protocol. The first view controller simply passes in a closure to the second view controller that the second view controller can then call before resigning itself.
Closures can be stored as variables to be called later. The following sets up an optional closure declaration in the second view controller class that could receive a Movie object and doesn’t return anything:
var saveMovie: ((Movie) -> Void)?
In the prepareForSegue
method, the first view controller would then pass the complete saveMovie
method into the second view controller as a closure:
secondViewController.saveMovie = { (_ movie: Movie) in self.moviesManager.addMovie(moive) }
Alternatively, the saveMovie
method itself could be passed in:
secondViewController.saveMovie = saveMovie
The second view controller can now directly call the saveMovie
method. Because closures capture variables from their original scope, when the second view controller calls the saveMovie
method, it will automatically have access to variables it refers to in the first view controller’s scope. Because the closure is declared as an optional, it must be unwrapped when called:
saveMovie?(movieToSave)
Unwind segue. Passing data from the second view controller to the first view controller is a bit tricky. There is no easy way to know which view controller has opened the current view controller, thus when we want to define a segue back to a specific view controller, we should create a special function in that particular view controller.
In our case, we want to pass the selected item to the first screen. To do so, we have to add a new function that will handle the reverse transition:
class FirstViewController: UIViewController { var data = "Hello from FirstViewController" override func viewDidLoad() { super.viewDidLoad() print(data) } @IBAction func unwindToFirstScreen(sender: UIStoryboardSegue) { print(data) } }
unwindToFirstScreen
function will be triggered when the unwind segue is fired. We have to pass the data from the second view controller to the first view controller. To create this unwind segue, you have to start creating a segue with a Ctrl drag from the button on the second view controller and drop it to the exit point. Then, we have to pick the exact function that should be triggered. The functions in the list are part of the other view controllers.
Following is code for SecondViewController
class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print("SecondViewController") } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let firstVS = segue.destination as! FirstViewController firstVS.data = "Hello from SecondViewController" } }
Loading a view controller via code
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { let controller = storyboard!.instantiateViewController( withIdentifier: "ListDetailViewController") as! ListDetailViewController controller.delegate = self let checklist = lists[indexPath.row] controller.checklistToEdit = checklist navigationController?.pushViewController(controller, animated: true) }
In this method, you create the view controller object and push it on to the navigation stack. This is roughly equivalent to what a segue would do behind the scenes. The view controller is embedded in a storyboard and you have to ask the storyboard object to load it.
The call to instantiateViewController(withIdentifier:)
takes an identifier string, ListDetailViewController
.
You still have to set this identifier on the navigation controller; otherwise the storyboard won't be able to find it. Open the storyboard and select the List Detail View Controller. Go to the Identity inspector and set Storyboard ID to ListDetailViewController
.
How to push and present to ViewController programmatically?
Switch between VC is really important for every application. You can switch between VC with three options :
Present View Controller
let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") self.present(vc, animated: true, completion: nil) // or safe way if let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") as? MovieVC { present(vc, animated: true, completion: nil) }
Push View Controller
let storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") navigationController?.pushViewController(vc, animated: true) // or safe way if let vc = storyboard.instantiateViewController(withIdentifier: "MovieVC") as? MovieVC, let navigator = navigationController { navigator.pushViewController(vc, animated: true) }
Useful links