Sunday, April 5, 2015

Passing Data Between View Controllers

Recently I found this question on StackOverflow:

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 tableview controller and I have enabled multiple selections.

Question:

My question is, how do I transfer the data from one view to another? I will be holding the selections on the tableview 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 AppDelegate. I read something about Singletons but 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?


There were some pretty good answers that addressed part of the question, but not all of it. The top voted answer gave a top notch explanation of how to pass information between two view controllers. But it did not make any mention of AppDelegate or Singleton. It also said next-to-nothing about a data model. None of the top answers considered what would be an appropriate solution if more than two view controllers needed to communicate to each other. This blog post fills in all these missing pieces.

I'm going to give a full answer. This post will discuss:
  • sharing data between two view controllers
  • sharing data between more than two view controllers
  • delegates
  • observer pattern
  • singletons
  • appDelegate

Application Scenarios

Rather than having a highly hypothetical, abstract discussion, it helps to have concrete applications in mind. To help define a two-view-controller situation and a more-than-two-view-controller situation, I am going to define two concrete application scenarios.

Scenario one: maximum two view controllers ever need to share information.
See diagram below.


There are two view controllers in the application. There is a ViewControllerA (Data Entry Form), and View Controller B (Product List). The items selected in the product list must match the items displayed in the text box in the data entry form. In this scenario, ViewControllerA and ViewControllerB must communicate directly with each other and no other view controllers. When the user is in the data entry form, and wishes to change the selected products, the application performs a transistion (in the case of storyboards, a segue is performed) from the data entry form to the product list. The product list must know which products were previously listed on the data entry form, to correctly show these items as already selected. Therefore as part of the transition / segue, there needs to be data passed from ViewControllerA to ViewControllerB.

Likewise, there is a reverse relationship. When the user finishes selecting products on the product list, the product list view controller is dismissed, and a transition occurs (or a segue) from the product list view controller back to the data entry form view controller. Before the data entry form can display itself properly, it needs to know about changes to the selected products. Therefore as part of the transition from ViewControllerB back to ViewControllerA, there needs to be data passed from ViewControllerB to ViewControllerA.

Scenario two: more than two view controllers need to share the same information.
See diagram two.

There are four view controllers in the application. It is a tab-based application for managing home inventory. Three view controllers present differently filtered views of the same data:
  • ViewControllerA - Luxury Items
  • ViewControllerB - Non-insured Items
  • ViewControllerC - Entire Home Inventory
  • ViewControllerD - Add New Item Form
Any time an individual item is created or edited, it must also synchronize with the other view controllers. For example, if we add a boat in ViewControllerD, but it is not yet insured, then the boat must appear when the user goes to ViewControllerA (Luxury Items), and also ViewControllerC (Entire Home Inventory), but not when the user goes to ViewControllerB (Non-insured Items). We need be concerned with not only adding new items, but also deleting items (which may be allowed from any of the four view controllers), or editing existing items (which may be allowed from the "Add New Item Form", repurosing the same for for editing).

Since all the view controllers do need to share the same data, all four view controllers need to remain in synchronization, and therefore there needs to be some sort of communication to all other view controllers, whenever any single view controller changes the underlying data. It should be fairly obvious that we do not want each view controller communicating directly with each other view controller in this scenario. In case it is not obvious, consider if we had 20 different view controllers (rather than just 4). How difficult and error prone would it be to notify each of the other 19 view controllers any time one view controller made a change?

The Solutions: Delegates and the Observer Pattern, and Singletons

In scenario one, we have two viable solutions:
  • Segues
  • Delegates
The Segue solution involves performing view controller transitions using Storyboards and segues. A property can be set on the destination view controller when the segue is triggered (see Apple's Developer Documentation: https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ManagingDataFlowBetweenViewControllers/ManagingDataFlowBetweenViewControllers.html).

The Delegate solution involves making one view controller the delegate of another. Typically the view controller on the top of the navigation stack, the one which was just presented, would NOT be the delegate, but rather would call delegate methods on the view controller which presented it.

In the scenario one, the Product List would have a delegate property, and the Data Entry Form would be the delegate. Being the delegate means conforming to a protocol by implementing the delegate methods, and then setting itself as the delegate before presenting the Data Entry Form.

In scenario two, we have other viable solutions:
  • Observer Pattern
  • Singletons
A singleton is an instance of a class, that instance being the only instance in existence during its lifetime. A singleton gets its name from the fact that it is the single instance. Normally developers who use singletons have special class methods for accessing them. An extra level of security to ensure that no one accidentally creates more than one instance at one time is to also override the init method. See the code below.

- (id) init {
    static BOOL alreadyInitialized = NO;
    if (alreadyInitialized) {
        // we want to prevent callers from "accidentally"
        // initializing more than one instance of this class
        return self;
    }
    alreadyInitialized = YES;
    
    self = [super init];
    if (self) {
        [self managedObjectContext];
    }
    return self;
}

+ (HouseholdInventoryManager*) sharedManager; {
    static dispatch_once_t onceQueue;
    static HouseholdInventoryManager* _sharedInstance;
    
    // dispatch_once is guaranteed to only be executed once in the
    // lifetime of the application
    dispatch_once(&onceQueue, ^{
        _sharedInstance = [[self alloc] init];
    });
    return _sharedInstance;
}

Now that we understand what a singleton is, let's discuss how a singleton fits into the observer pattern. The observer pattern is used for one object to respond to changes by another object. In the second scenario, we have four different view controllers, who all want to know about changes to the underlying data. The "underlying data" should belong to a single instance, a singleton. The "know about changes" is accomplished by observing changes made to the singleton.

Putting these pieces together, for scenario two we would have various model classes representing the household inventory items. For example, a household inventory item may look like the following class definition.

@interface JGCHouseholdInventoryItem : NSObject
@property (nonatomic, copy) NSString * itemName;
@property (nonatomic, strong) NSNumber * originalPrice;
@property (nonatomic, copy) NSDate * purchaseDate;
@property (nonatomic, strong) UIImage * photo;
@property (nonatomic, copy) NSString * itemDescription;
@property (nonatomic, assign, getter=isLuxuryItem) BOOL luxuryItem;
@property (nonatomic, assign, getter=isInsured) BOOL insured;
@end

The home inventory application would have a single instance of a class which is designed to manage a list of inventory items. In the iOS world, a singleton that performs a management functionality is often called a "manager". So the class method for getting the single data manager instance is called "sharedManager". The manager would manage a collection of household items. The following is a class definition for the data manager:

#import <Foundation/Foundation.h>

@class JGCHouseholdInventoryItem;

@interface HouseholdInventoryManager : NSObject
/*!
 The global singleton for accessing application data
 */
+ (HouseholdInventoryManager*) sharedManager;

- (NSArray *) entireHouseholdInventory;
- (NSArray *) luxuryItems;
- (NSArray *) nonInsuredItems;

- (void) addHouseholdItemToHomeInventory:(JGCHouseholdInventoryItem*)item;
- (void) editHouseholdItemInHomeInventory:(JGCHouseholdInventoryItem*)item;
- (void) deleteHoueholdItemFromHomeInventory:(JGCHouseholdInventoryItem*)item;
@end

When the collection of home inventory items changes, the view controllers need to be made aware of this change. The class defintion above does not make it obvious how this will happen. We need to follow the observer pattern. The view controllers must formally observe the sharedManager. There are two ways to observe another object:
  • Key-Value-Observing (KVO)
  • NSNotificationCenter
In scenario two, we do not have a single property of the HouseholdInventoryManager which could be observed using KVO. We have purposefully kept the array of household items internal. In fact, we could internally be using a dictionary or a set to store the household items, and just providing arrays when the getter methods are called. Because we do not have a single property which is easily observable, the observer pattern in this case must be implemented using NSNotificationCenter. Each of the four view controllers would subscribe to notifications, and the sharedManager would send notifications to the notification center when appropriate. The inventory manager does not need to know anything about the view controllers or instances of any other classes which may be interested in knowing when the collection of inventory items changes; the NSNotificationCenter takes care of these implementation details. The View Controllers simply subscribe to notifications, and the data manager simply posts notifications.

For more information on Observers and Delegates see this post: http://jasoncross-ios-development.blogspot.com/2015/04/asynchronous-json-requests-in-objective.html.

Application Delegate and Singletons

The original poster of the question on StackOverflow had seen some people declare an array in the AppDelegate. This equates to taking a collection of model objects (the array) and placing it in a singleton (the AppDelegate). This is similar to the solution proposed above, but certainly not identical.

Many beginner programmers take advantage of the fact that there is always exactly one Application Delegate in the lifetime of the application, which is globally accessible. Beginning programmers use this fact to stuff objects and functionality into the appDelegate as a convenience for access from anywhere else in the application. Just because the AppDelegate is a singleton doesn't mean it should replace all other singletons. This is a poor practice as it places too much burden on one class, breaking good object oriented practices. Each class should have a clear role that is easily explained, often just by the name of the class.

Any time your Application Delegate starts to get bloated, start to remove functionality into singletons. For example, the Core Data Stack should not be left in the AppDelegate, but should instead be put in its own class, a coreDataManager class. The final solution proposed in this blog post has a clear separation of duties between classes, minimizing interactions and dependencies.
AppDelegate
kept lean and mean to perform only core duties related to the application and the OS
ViewControllers
control the views they contain, passing along model information as needed. Do not need to know about every other view controller.
Model
used to represent the data which the ViewControllers ask the views to present. Bare bones classes that store data.
Data Manager
HouseholdInventoryManager. A singleton which manages the data for the entire application. Knows about the model objects, and transforms the data as needed (filtering when asked). Knows nothing about the ViewControllers.
Notification Center
manages an internal list of notifications: keeps track of observers. When notifications are posted, passes these along to the subscribed observers. Knows about the ViewControllers because they have added themselves as observers. Does not necessarily know anything about the HouseholdInventoryManager (just processes post request from any object).
Now the original question has been fully answered: the reader should now know about passing information between view controllers in two different scenarios (only two or more than two view controllers needing to communicate), what is a singleton and how to create one and when to use one, and finally the appropriate use of the AppDelegate.

No comments:

Post a Comment