This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 2 years ago.
I am creating a library in IOS/swift that:
takes a user to a scene --> performs a task --> return to the initial scene that called the first while passing a payload back to the user
I have figured out how to take users back to the previous scene that called it, but my issue is how to send a payload back with it using thee code snippet below:
func switchToPreviousPage(){
self.dismiss(animated: true, completion: nil)
}
How do I achieve this?
In your scenario you can use either :
Delegation Pattern
Notification/Observer
Lets discuss each one :
1. Delegation :
If you have idea about Protocol in Swift you can do it easily.
first create a protocol with the required function you want to implement :
protocol FirstControllerDelegate: AnyObject {
func sendData(data: String)
}
Suppose your firstPage is FirstViewController, it has a UILabel and we have to assign a String to it from our secondPage means SecondViewController. the Structure of your FirstViewController may be like this :
class FirstViewController: UIViewController {
#IBOutlet weak var textLabel: UILabel!
#IBAction func gotoSecondPage() {
let secondVC = storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
}
}
Now your FirstViewController has to confirm to this protocol and it will implement the sendData(data: ) method :
extension FirstViewController: FirstControllerDelegate {
func sendData(data: String) {
textLabel.text = data
}
}
Now as a feature of Protocol in iOS, Protocols can work as a Type(like Int, String). So just create a variable of type FirstControllerDelegate in your SecondViewController !
class SecondViewController: UIViewController {
weak var delegate: FirstControllerDelegate!
#IBAction func switchToPreviousPage() {
delegate.sendData(data: "Hello")
self.dismiss(animated: true, completion: nil)
}
}
You can now call the sendData(data:) function with the variable you created above !
At last you have to do oneThing just assign the delegate :
secondVC.delegate = self
It should be inside the gotoSecondPage() method !
2. Notification/Observer
With this, our basic idea is to send a Notification inside our app, and it can be observed by any where inside !
So our SecondViewController will send a Notification embedded with required data that we want to pass, and FirstViewController will receive the Notification and it will extract the data from the Notification !!
Each Notification has a specific name, which will differentiate it from other Notifications. we have to create the Name :
Notification.Name(rawValue: "com.app.notificationObserver")
Now the FirstViewController will be Observe to this specific notification :
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.changeLabelText(notifcation:)), name: Notification.Name("com.app.notificationObserver"), object: nil)
}
We have to define changeLabelText(notification:) method :
private func changeLabelTExt(notification: NSNotification) {
if let dataDict = notification.userInfo as NSDictionary? {
if let message = dataDict["data"] as? String {
self.textLabel.text = message
}
}
}
Finally, SecondViewController will trigger the Notification :
#IBAction func switchToPreviousPage() {
NotificationCenter.default.post(name: Notification.Name(rawValue: "com.app.notificationObserver"), object: ["data": "hello"])
self.dismiss(animated: true, completion: nil)
}
Thats All .....
Suppose I have a storyboard like so:
Is it possible for me to get a flag or a boolean data from A back to B? I initially thought of using delegation but most of the tutorials about it talks about sending data between UIViewControllers that are part of 1 NavigationController. In my case, the UIViewController I need to get data is outside of the navigation controller. Is there a way for me to send data from A to B despite not being embedded in the same NavigationController?
If you don't want to use delegate between the classes . One possible way is to create separated file , saved in class and fetch required data any where in navigation .
Useful class for your case would be create singleton class FlowEngine . Use getter / setter method for saving and fetching of data. Code is attached for your reference .
class FlowEngine : NSObject{
private let static shared = FlowEngine()
private var data : String
private init(){
}
func savedData(text : String){
data = text
}
func fetchSavedData() -> String{
return data // add checsk for nil values
}
}
Delegation doesn't require the ViewControllers to be in same navigation stack. You can use the same for your case. However, if you choose to go with NotificationCenter, just remember to remove the observer when appropriate.
Other answers seem to accomplish your requirements but for the sake of completeness you could try to use KVC and KVO for modifying values in A and receiving its changes in B (or any other place)
You could see a detailed explanation of how to use them in here.
You have several ways to go, depending on your needs :
Delegation
Declare a protocol in A, and make B conform to it. Set the delegate of A to B. This could be cumbersome if the navigation stack has too many level, as you would need to pass the reference of B to each ViewController between A & B
Notification / KVO
B subscribe to a notification sent by A, no reference needed, thread safe. Don't forget to unsubscribe when done.
Proxy class
Use a proxy singleton class, that will hold your data. A will write to it, and B will read it in viewWillAppear.
UserDefaults
Same concept as a Proxy Class, but the data will persist during your app life cycle and even after killing the app. It's appropriate if you want to change a flag or a setting for your user, not if you have a lot of data to hold.
Cocoa Touch uses the target-action mechanism for communication between a control and another object. More here... If you would like to use it with UIControl objects like buttons, then you can set it in Interface Builder by sending an action to the FirstResponder object.
Target-Action will start searching a VC which responds to a given method from the current first responder and then will move to the next responder and will terminate a search in a current UIWindow. Once a controller which responds to a method signature is found, the search is terminated.
class AViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func configure(with dictionary: Dictionary<String, Any>) {
print(dictionary)
}
}
class BViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let a = self.targetViewController(forAction: #selector(ViewController.configure(with:)), sender: self) as? ViewController
a?.configure(with: ["firstName": "Alex", "lastName": "Toto"])
}
}
if your A viewController is not huge, In B viewController do this :
class B : UIViewController {
var a : A! = nil
func viewDidLoad() {
super.viewDidLoad()
a = storyboard?.instantiateViewController(withIdentifier: "StoryBoard ID") as? A
if a.booleanValue == true {
// use your booleanValue
a = nil // deallocate after using your value.
}
}
}
Update (better solution)
We've had to edit a few things to the functionality which presented me with the opportunity to refactor this. I used the NSNotification way, which was way cleaner than using closures.
ViewControllerB
override func viewDidLoad() {
super.viewDidLoad()
//Observe for notification from "myIdentifier"
NotificationCenter.default.addObserver(self, selector: #selector(self.processNotification(notification:)), name: Notification.Name("myIdentifier"), object: nil)
}
//function that gets called when notification is received
//the #objc annotation is required!
#objc func processNotification(notification: Notification) {
//Do something
}
ViewControllerA
#IBAction func didTapButton(_ sender: Any) {
//Process something
// ...
//
//Post a notification to those observing "myIdentifier"
NotificationCenter.default.post(name: Notification.Name("myIdentifier"), object: nil)
self.dismiss(animated: true, completion: nil)
}
Old (but working) solution
This might be an unpopular solution but I managed to solve this with callbacks. I was looking into another possible solution which was commented NSNotification but since someone from the team already had experience with using callbacks in this manner, we decided to ultimately use that.
How we made it work:
ViewControllerB is given the actual code implementation through prepare(for segue: UIStoryboardSegue, sender: Any?) while ViewControllerC (This is the middle UIViewController in the picture) has a callback property and ViewControllerA contains the value to pass when it's about to be dismissed.
ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "secondSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[0] as! ViewControllerC
vc.completion = { hasAgreed in
//Do Something
}
}
}
ViewControllerC
class ViewControllerC: UIViewController {
var completion: ((Bool) -> ())?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "thirdSegue" {
let nvc: NavigationController = segue.destination as! NavigationController
let vc = nvc.viewControllers[1] as! ViewControllerA
vc.middleController = self
}
}
ViewControllerA
class ViewControllerC: UIViewController {
var middleController: ViewControllerC?
#IBAction func didTapButton(_ sender: Any) {
self.dismiss(animated: true, completion: {
middleController?.completion(true)
})
}
}
With this, we got the data we needed from the diagram picture above.
Your best bet is to make use of NotificationCenter to achieve this.
Post notification like this:
NotificationCenter.default.post(name: Notification.Name("NotificationName"), object: nil, userInfo: ["somekey":"somevalue"])
Observe it like this:
NotificationCenter.default.addObserver(self, selector: #selector(self.dataReceived(notification:)), name: Notification.Name("NotificationName"), object: nil)
Use the following method:
#objc func dataReceived(notification: Notification) {}
I want to call a web service whenever application coming back in the foreground. I am calling it from didBecomeActive().
What's the best way to handle it and pass data to Root view controller?
Since the data you want to pass is always going to the same view controller you should instead set the observer in that view controller instead of app delegate. This way you won't need to pass any data in the first place.
class YourViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default
.addObserver(self, selector: #selector(activityHandler(_:)),
name: UIApplication.didBecomeActiveNotification, object: nil)
}
#objc func activityHandler(_ notification: Notification) {
//Call your web service here
}
}
You have two choices. Get rootViewController and pass the data, handle it.
func applicationDidBecomeActive(_ application: UIApplication) {
// 1
let rootVC1 = self.window?.rootViewController
// 2
let rooVC2 = application.windows.first?.rootViewController
...
/*
pass data to rootVC1 or rootVC2
*/
}
I have a two ViewControllers: ViewController and SecondViewController.
I added an observer to this two ViewControllers. In ViewController I also defined an IBAction to post the notification. I handle the notification via a closure in both ViewControllers. But only the closure in the ViewController gets called. The closure (and even the whole code) in the SecondViewController does not get called (I checked with debugger). The closure only contains a print-statement.
Here is my Code
//ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nc = NotificationCenter.default
nc.addObserver(forName: Notification.Name(rawValue:"MyNotification"), object: nil, queue: nil) { (notification) in
print("I'm the : \(type(of: self))")
}
}
#IBAction func sendNotification(_ sender: UIButton) {
let nc = NotificationCenter.default
nc.post(name: Notification.Name(rawValue:"MyNotification"), object: nil, userInfo: ["message":"Hello there!", "Date:":Date()])
}
}
The ScondViewController
//SecondViewController
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let nc = NotificationCenter.default
nc.addObserver(forName: Notification.Name(rawValue:"MyNotification"), object: nil, queue: nil) { (notification) in
print("I'm the: \(type(of: self))")
}
}
}
The closure in ViewController gets called but the closure in SecondViewController does not. Maybe the reason is that SecondViewController does not get initialized before I post the notification. But how would a solution look like?
Any help is appreciated.
If this is one-to-one relationship you can also use the Delegate pattern instead. Although you are right, the notification observer is not yet called because you do not initialise before the post. So there shouldn't be a reason for a function to be called in the SecondViewController.
So if you only need that to update a variable value, you can do that somewhere else in a Variables class where both ViewControllers have access.
Here is my code:
I am making the user to be able to select image from phone and then want to jump back to the previous view controller passing this image file too..
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
if (segue.identifier == "unwindToThis") {
}
}
If your original view controller is still loaded in memory you can use an NSNotification via NSNotificationCenter.
In the view controller that needs the data, in viewDidLoad:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "imageReceived:", name: "imageReceived", object: self.videoDeviceInput?.device)
Then add the deinit method to the same controller, this will remove the observer when the view controller is no longer loaded.
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "imageReceived", object: nil)
}
Still in the same class create a method to handle the notification:
func imageReceived(notification: NSNotification) {
if let image = notification.object as? UIImage {
// Do your image stuff here.
}
}
In the view controller where you create or get the data you need to pass back you can to trigger the notification with the data like so:
NSNotificationCenter.defaultCenter().postNotificationName("imageReceived", object: yourUIImage)