Categories
ios model-view-controller objective-c swift uiviewcontroller

Passing data between view controllers

1481

I’m new to iOS and Objective-C and the whole MVC paradigm and I’m stuck with the following:

I have a view that acts as a data entry form and I want to give the user the option to select multiple products. The products are listed on another view with a UITableViewController and I have enabled multiple selections.

How do I transfer the data from one view to another? I will be holding the selections on the UITableView in an array, but how do I then pass that back to the previous data entry form view so it can be saved along with the other data to Core Data on submission of the form?

I have surfed around and seen some people declare an array in the app delegate. I read something about singletons, but I don’t understand what these are and I read something about creating a data model.

What would be the correct way of performing this and how would I go about it?

0

    1744

    This question seems to be very popular here on Stack Overflow so I thought I would try and give a better answer to help out people starting in the world of iOS like me.

    Passing Data Forward

    Passing data forward to a view controller from another view controller. You would use this method if you wanted to pass an object/value from one view controller to another view controller that you may be pushing on to a navigation stack.

    For this example, we will have ViewControllerA and ViewControllerB

    To pass a BOOL value from ViewControllerA to ViewControllerB we would do the following.

    1. in ViewControllerB.h create a property for the BOOL

       @property (nonatomic, assign) BOOL isSomethingEnabled;
      
    2. in ViewControllerA you need to tell it about ViewControllerB so use an

       #import "ViewControllerB.h"
      

    Then where you want to load the view, for example, didSelectRowAtIndex or some IBAction, you need to set the property in ViewControllerB before you push it onto the navigation stack.

        ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNib:@"ViewControllerB" bundle:nil];
        viewControllerB.isSomethingEnabled = YES;
        [self pushViewController:viewControllerB animated:YES];
    

    This will set isSomethingEnabled in ViewControllerB to BOOL value YES.

    Passing Data Forward using Segues

    If you are using Storyboards you are most likely using segues and will need this procedure to pass data forward. This is similar to the above but instead of passing the data before you push the view controller, you use a method called

    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    

    So to pass a BOOL from ViewControllerA to ViewControllerB we would do the following:

    1. in ViewControllerB.h create a property for the BOOL

       @property (nonatomic, assign) BOOL isSomethingEnabled;
      
    2. in ViewControllerA you need to tell it about ViewControllerB, so use an

       #import "ViewControllerB.h"
      
    3. Create the segue from ViewControllerA to ViewControllerB on the storyboard and give it an identifier. In this example we’ll call it "showDetailSegue"

    4. Next, we need to add the method to ViewControllerA that is called when any segue is performed. Because of this we need to detect which segue was called and then do something. In our example, we will check for "showDetailSegue" and if that’s performed, we will pass our BOOL value to ViewControllerB

       -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
           if([segue.identifier isEqualToString:@"showDetailSegue"]){
               ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
               controller.isSomethingEnabled = YES;
           }
       }
      

    If you have your views embedded in a navigation controller, you need to change the method above slightly to the following

        -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
            if([segue.identifier isEqualToString:@"showDetailSegue"]){
                UINavigationController *navController = (UINavigationController *)segue.destinationViewController;
                ViewControllerB *controller = (ViewControllerB *)navController.topViewController;
                controller.isSomethingEnabled = YES;
            }
        }
    

    This will set isSomethingEnabled in ViewControllerB to BOOL value YES.

    Passing Data Back

    To pass data back from ViewControllerB to ViewControllerA you need to use Protocols and Delegates or Blocks, the latter can be used as a loosely coupled mechanism for callbacks.

    To do this we will make ViewControllerA a delegate of ViewControllerB. This allows ViewControllerB to send a message back to ViewControllerA enabling us to send data back.

    For ViewControllerA to be a delegate of ViewControllerB it must conform to ViewControllerB‘s protocol which we have to specify. This tells ViewControllerA which methods it must implement.

    1. In ViewControllerB.h, below the #import, but above @interface you specify the protocol.

       @class ViewControllerB;
      
       @protocol ViewControllerBDelegate <NSObject>
       - (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item;
       @end
      
    2. Next still in the ViewControllerB.h, you need to set up a delegate property and synthesize in ViewControllerB.m

       @property (nonatomic, weak) id <ViewControllerBDelegate> delegate;
      
    3. In ViewControllerB we call a message on the delegate when we pop the view controller.

       NSString *itemToPassBack = @"Pass this value back to ViewControllerA";
       [self.delegate addItemViewController:self didFinishEnteringItem:itemToPassBack];
      
    4. That’s it for ViewControllerB. Now in ViewControllerA.h, tell ViewControllerA to import ViewControllerB and conform to its protocol.

       #import "ViewControllerB.h"
      
       @interface ViewControllerA : UIViewController <ViewControllerBDelegate>
      
    5. In ViewControllerA.m implement the following method from our protocol

       - (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item
       {
           NSLog(@"This was returned from ViewControllerB %@", item);
       }
      
    6. Before pushing viewControllerB to navigation stack we need to tell ViewControllerB that ViewControllerA is its delegate, otherwise we will get an error.

       ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNib:@"ViewControllerB" bundle:nil];
       viewControllerB.delegate = self
       [[self navigationController] pushViewController:viewControllerB animated:YES];
      

    References

    1. Using Delegation to Communicate With Other View Controllers in the View Controller Programming Guide
    2. Delegate Pattern

    NSNotification center

    It’s another way to pass data.

    // Add an observer in controller(s) where you want to receive data
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDeepLinking:) name:@"handleDeepLinking" object:nil];
    
    -(void) handleDeepLinking:(NSNotification *) notification {
        id someObject = notification.object // Some custom object that was passed with notification fire.
    }
    
    // Post notification
    id someObject;
    [NSNotificationCenter.defaultCenter postNotificationName:@"handleDeepLinking" object:someObject];
    

    Passing Data back from one class to another (A class can be any controller, Network/session manager, UIView subclass or any other class)

    Blocks are anonymous functions.

    This example passes data from Controller B to Controller A

    Define a block

    @property void(^selectedVoucherBlock)(NSString *); // in ContollerA.h
    

    Add block handler (listener)

    Where you need a value (for example, you need your API response in ControllerA or you need ContorllerB data on A)

    // In ContollerA.m
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        __unsafe_unretained typeof(self) weakSelf = self;
        self.selectedVoucherBlock = ^(NSString *voucher) {
            weakSelf->someLabel.text = voucher;
        };
    }
    

    Go to Controller B

    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ControllerB *vc = [storyboard instantiateViewControllerWithIdentifier:@"ControllerB"];
    vc.sourceVC = self;
        [self.navigationController pushViewController:vc animated:NO];
    

    Fire block

    -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
    (NSIndexPath *)indexPath {
        NSString *voucher = vouchersArray[indexPath.row];
        if (sourceVC.selectVoucherBlock) {
            sourceVC.selectVoucherBlock(voucher);
        }
        [self.navigationController popToViewController:sourceVC animated:YES];
    }
    

    Another Working Example for Blocks

    19

    • 25

      Do we also have to put an @class ViewControllerB; above the @protocol definition? Without it I get an “Expected type” error on ViewControllerB in the line: - (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item; within the @protocol declaration

      – alan-p

      Aug 30, 2012 at 13:16


    • 4

      This works great. As alan-p says, don’t forget to write @class ViewControllerB; above the protocol otherwise you’ll receive “Expected a type” error.

      Nov 22, 2012 at 11:11


    • 7

      you don’t need delegates for passing back, just use unwind.

      – malhal

      Sep 27, 2013 at 3:30

    • 4

      When I put “viewControllerB.delegate = self;” in ViewControllerB I’m getting an error. Assigning to ‘id<ViewControllerBDelegate>’ from incompatible type ‘ViewControllerB *const __strong’, I’m not sure what I’m doing wrong. Can anyone help? Plus I had to change: initWithNib –> initWithNibName.

      Mar 28, 2014 at 21:21


    • 4

      if you are using NavigationController you have to use [self.navigationController pushViewController:viewController animated:YES]; instead [self pushViewController:viewControllerB animated:YES];

      – Nazir

      Jun 5, 2014 at 5:26


    222

    Swift

    There are tons and tons of explanations here and around Stack Overflow, but if you are a beginner just trying to get something basic to work, try watching this YouTube tutorial (It’s what helped me to finally understand how to do it).

    Passing data forward to the next View Controller

    The following is an example based on the video. The idea is to pass a string from the text field in the First View Controller to the label in the Second View Controller.

    Enter image description here

    Create the storyboard layout in the Interface Builder. To make the segue, you just Control click on the button and drag over to the Second View Controller.

    First View Controller

    The code for the First View Controller is

    import UIKit
    
    class FirstViewController: UIViewController {
    
        @IBOutlet weak var textField: UITextField!
    
        // This function is called before the segue
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    
            // Get a reference to the second view controller
            let secondViewController = segue.destination as! SecondViewController
    
            // Set a variable in the second view controller with the String to pass
            secondViewController.receivedString = textField.text!
        }
    
    }
    

    Second View Controller

    And the code for the Second View Controller is

    import UIKit
    
    class SecondViewController: UIViewController {
    
        @IBOutlet weak var label: UILabel!
    
        // This variable will hold the data being passed from the First View Controller
        var receivedString = ""
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Used the text from the First View Controller to set the label
            label.text = receivedString
        }
    
    }
    

    Don’t forget

    • Hook up the outlets for the UITextField and the UILabel.
    • Set the first and second View Controllers to the appropriate Swift files in Interface Builder.

    Passing data back to the previous View Controller

    To pass data back from the second view controller to the first view controller, you use a protocol and a delegate. This video is a very clear walk though of that process:

    The following is an example based on the video (with a few modifications).

    Enter image description here

    Create the storyboard layout in the Interface Builder. Again, to make the segue, you just Control drag from the button to the Second View Controller. Set the segue identifier to showSecondViewController. Also, don’t forget to hook up the outlets and actions using the names in the following code.

    First View Controller

    The code for the First View Controller is

    import UIKit
    
    class FirstViewController: UIViewController, DataEnteredDelegate {
    
        @IBOutlet weak var label: UILabel!
    
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "showSecondViewController" {
                let secondViewController = segue.destination as! SecondViewController
                secondViewController.delegate = self
            }
        }
    
        func userDidEnterInformation(info: String) {
            label.text = info
        }
    }
    

    Note the use of our custom DataEnteredDelegate protocol.

    Second View Controller and Protocol

    The code for the second view controller is

    import UIKit
    
    // Protocol used for sending data back
    protocol DataEnteredDelegate: AnyObject {
        func userDidEnterInformation(info: String)
    }
    
    class SecondViewController: UIViewController {
    
        // Making this a weak variable, so that it won't create a strong reference cycle
        weak var delegate: DataEnteredDelegate? = nil
    
        @IBOutlet weak var textField: UITextField!
    
        @IBAction func sendTextBackButton(sender: AnyObject) {
    
            // Call this method on whichever class implements our delegate protocol
            delegate?.userDidEnterInformation(info: textField.text!)
    
            // Go back to the previous view controller
            _ = self.navigationController?.popViewController(animated: true)
        }
    }
    

    Note that the protocol is outside of the View Controller class.

    That’s it. Running the app now, you should be able to send data back from the second view controller to the first.

    12

    • Given some of the latest Swift updates, is this still a common pattern to implement?

      – piofusco

      Dec 8, 2015 at 7:35

    • 4

      Most all of the Swift updates that I’ve seen have been relatively minor syntactical changes, not changes in how data is passed between view controllers. If I do learn of any major changes like that, I’ll update my answer.

      – Suragch

      Dec 8, 2015 at 11:23

    • 2

      offtopic – iOS has such an ugly way to pass parameters to new view controllers, unbelievable – you have to set parameters not in a place when you’re making the call, but in some other one. Android has a better approach in this regard – when you start an Activity you can pass any data (well, almost) via its starting Intent. Easy. No need to cast or something. Passing return values back to caller is an essential thing too, no need to delegate. Of course it is possible to use ugly approaches too, no problem there ))

      – Mixaz

      Apr 1, 2016 at 15:51

    • 1

      @Himanshu, first get a reference to the second view controller. Then update the public variable that it contains.

      – Suragch

      Jul 30, 2016 at 15:26

    • 8

      @Honey. I think the word “delegate” is confusing. Let me use the word “worker”. The “worker” (first view controller) does whatever the “boss” (second view controller) tells it to do. The “boss” doesn’t know who its “worker” will be; it could be anyone. So in the first view controller (“worker” class), it says, I will be your “worker”. You tell me what to write in the label and I’ll do it for you. Thus, secondViewController.delegate = self means “I agree to be the boss’s worker.” See this answer for another example and more explanation.

      – Suragch

      Sep 26, 2016 at 6:44

    142

    The M in MVC is for “Model” and in the MVC paradigm the role of model classes is to manage a program’s data. A model is the opposite of a view — a view knows how to display data, but it knows nothing about what to do with data, whereas a model knows everything about how to work with data, but nothing about how to display it. Models can be complicated, but they don’t have to be — the model for your app might be as simple as an array of strings or dictionaries.

    The role of a controller is to mediate between view and model. Therefore, they need a reference to one or more view objects and one or more model objects. Let’s say that your model is an array of dictionaries, with each dictionary representing one row in your table. The root view for your app displays that table, and it might be responsible for loading the array from a file. When the user decides to add a new row to the table, they tap some button and your controller creates a new (mutable) dictionary and adds it to the array. In order to fill in the row, the controller creates a detail view controller and gives it the new dictionary. The detail view controller fills in the dictionary and returns. The dictionary is already part of the model, so nothing else needs to happen.

    0