Reputation: 3698
I would appreciate any help in solving my problem that has had me pulling my hair out for the last day!
Disclaimer 1 I'm an Objective-C noob - so the answer to this may be very simple (I'm coming from years of PHP). Please be nice.
Disclaimer 2 Yes, I have extensively 'Googled' and looked at Apple Documentation - but have failed to come to an understanding of my problem.
Summary
I am trying to pass a value selected in a table view to another controller from a singleton. I am finding that the viewDidLoad method in the second view controller is completing before the tableView: didSelectRowAtIndexPath: method which triggered the segue. This is leading to the second view controller access 'stale' data.
Detail
I have set up a project with a very simple StoryBoard using ARC targeting iOS5.
I have created a singleton to store a NSString between view controllers.
Singleton.h
#import <Foundation/Foundation.h>
@interface Singleton : NSObject
@property (strong, nonatomic) NSString *sharedString;
+ (id)singletonManager;
@end
Singleton.m
#import "Singleton.h"
static Singleton *sharedInstance = nil;
@implementation Singleton
@synthesize sharedString = _sharedString;
+(id)singletonManager
{
@synchronized(self){
if (sharedInstance == nil)
sharedInstance = [[self alloc] init];
}
return sharedInstance;
}
-(id) init {
if (self = [super init]) {
_sharedString = [[NSString alloc] initWithString:@"Initialization String"];
}
return self;
}
@end
The TableView in the story board is linked to the following TableViewController
TableViewController.h
#import <UIKit/UIKit.h>
@interface TableViewController : UITableViewController
@end
TableViewController.m
#import "TableViewController.h"
#import "Singleton.h"
@interface TableViewController ()
@property NSArray *tableData;
@end
@implementation TableViewController
@synthesize tableData;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableData = [NSArray arrayWithObjects:@"First", @"Second", @"Third", nil];
}
- (void)viewDidUnload
{
[super viewDidUnload];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [self.tableData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
cell.textLabel.text = [self.tableData objectAtIndex:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"TableViewController: didSelectRowAtIndexPath start");
Singleton *sharedInstance = [Singleton singletonManager];
sharedInstance.sharedString = [self.tableData objectAtIndex:indexPath.row];
NSLog(@"TableViewController: Finished storing data into shared string: %@", sharedInstance.sharedString);
NSLog(@"TableViewController: didSelectRowAtIndexPath start");
}
@end
The table cell is segue-ed (push) to a basic ViewController with a single label.
DetailViewController.h
#import <UIKit/UIKit.h>
@interface DetailViewController : UIViewController
@property (strong, nonatomic) IBOutlet UILabel *displayLabel;
@end
DetailViewController.m
#import "DetailViewController.h"
#import "Singleton.h"
@interface DetailViewController ()
@end
@implementation DetailViewController
@synthesize displayLabel = _displayLabel;
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"DetailViewController: viewDidLoad start");
Singleton *sharedInstance = [Singleton singletonManager];
self.displayLabel.text = sharedInstance.sharedString;
NSLog(@"displayLabel.text: %@", self.displayLabel.text);
NSLog(@"DetailViewController: viewDidLoad end");
}
- (void)viewDidUnload
{
[self setDisplayLabel:nil];
[super viewDidUnload];
}
@end
When a table cell is selected, I am wanting
Unfortunately this is not working as expected with my code. If I look at the NSLog details in the console I can see that the detail view's viewDidLoad function is actually finishing before the initiating table view method (didSelectRowAtIndexPath).
2012-05-10 23:17:45.756 TestSingleton[12105:f803] DetailViewController: viewDidLoad start
2012-05-10 23:17:45.758 TestSingleton[12105:f803] displayLabel.text: Initialization String
2012-05-10 23:17:45.759 TestSingleton[12105:f803] DetailViewController: viewDidLoad end
2012-05-10 23:17:45.763 TestSingleton[12105:f803] TableViewController: didSelectRowAtIndexPath start
2012-05-10 23:17:45.764 TestSingleton[12105:f803] TableViewController: Finished storing data into shared string: First
2012-05-10 23:17:45.765 TestSingleton[12105:f803] TableViewController: didSelectRowAtIndexPath start
Arrrrggghh!
If I place the setting of the label text in the viewDidAppear method of the detail view, the correct value from shared singleton NSString will be displayed. With this approach, however, you can actually see the app changing the value of the text label (eg. if you click on the first row, when the detail view screen loads you can SEE the label change from "Initialization String" to "First").
Can somebody please explain how I can pass a shared value from a singleton to another view and have it available to the second view controller before the view is actually rendered to screen.
If any clarification is required - please let me know.
I'm sure this has something to do with my failure to appreciate the subtleties of the view lifecycle.
UPDATE
Thanks to *8vius.
My new method in TableViewController.m which solves my problem:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:@"mySegue"]) {
NSLog(@"Preparing for Segue to detail view");
Singleton *sharedInstance = [Singleton singletonManager];
NSIndexPath *selectedRow = [self.tableView indexPathForSelectedRow];
sharedInstance.sharedString = [self.tableData objectAtIndex:selectedRow.row];
NSLog(@"TableViewController: Finished storing data into shared string: %@", sharedInstance.sharedString);
}
}
Upvotes: 4
Views: 2524
Reputation: 5836
In your destination view controller set up a property (public) so you can set the value you wish to pass in. Implement prepareForSegue
in your origin view controller and import the header of your destination view controller.
To get the indexPath of a table view cell you should have an IBOutlet set to your table view, this way you can get the indexPath using the indexPathForCell method in your prepareForSegue method.
In your prepareForSegue
you should have something like this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if ([segue.identifier isEqualToString:@"[identifier for your segue]"]) {
[segue.destinationViewController setValueIWantToSet:value];
}
}
You could also set the value of the label text here, if you wish.
The reason your viewDidLoad
finishes before is because iOS sets up the destination view controller before transitioning to it, using the prepareForSegue
method guarantees that your destination view controller is already instantiated and set up, so you can set the values you wish.
Upvotes: 6