views:

292

answers:

3

I have a CoreData model that contains Levels which can again contain childLevels.

I represent each level with a UITableViewController listing all childLevels. When a user taps a row a new UITableViewController is pushed onto the navigationController. No problem here.

How would I go about storing the user location within this table structure. Is there a best practice for doing this? I have no problem doing this if the depth of the structure was known, but somehow puzzled how to approach this with a undefined depth.

Should I store the NSIndexPath tapped by the user into an array and write it to disk?

+1  A: 

Using an NSIndexPath for your state and saving/restoring it for persistence makes sense to me.

Also your approach--to use an NSArray stored as a property list (plist)--should be fairly straightforward.

gerry3
I will probably do that and roll my own UINavigationController to keep a correct array of IndexPaths. Or is it possible to do this all within one NSIndexPath object?
Felix
I would just use one. Actually you could just use one NSMutableArray instead of the NSIndexPath since you can save and restore the array directly.
gerry3
I went with the objectIDs of the root ManagedObjects of each viewController instead of the tapped NSIndexPaths. I guess this is safer in case the order changed, it is also more efficient as I do not need to perform the fetchRequest for each viewController and can retrieve the roots directly. Will try and post my code later. I managed to do everything in the navigationController subclass.
Felix
I posted my code below.
Felix
A: 

I'm just about ready to start doing this for my own app and here's the plan. Each time a UIViewController loads a new view it will create a value in the NSUserDefaults that gives information on that new view (where it was opened from, what data it's populated with, etc). When the subview returns the view controller clears out this value. When the UIViewController has it's initial load it checks the defaults to see if it previously stored a value, if so, it takes the appropriate action to reload that subview. The process continues down the navigation chain.

The reason I've done this instead of an NSIndexPath is because, in addition to the main view hierarchy, I've got a bunch of auxiliary views (add, remove, edit, etc.). Not only will I need to record the opening of these views that exist outside of the main navigation, but they will have a lot of state details they need to save too (selected options, partially entered text, etc.)

I'll come back here and downvote this if it turns out to be a shit plan.

kubi
I would prefer to do this outside of viewDidLoad, to avoid loading views that are not visible.
Felix
I'm concerned about that too, but I'm going to do it this way for round 1. If it's annoying, I'll move the checks up earlier in the instantiation process.Do you happen to know if the `viewDidLoad` method fires if you've already pushed a new view controller?
kubi
I think that the viewDidLoad just before viewWillAppear. So only the last viewController pushed onto the navigation stack should receive this message, while the other viewControllers only get viewDidLoad once they are presented to the user.
Felix
I posted my code below.
Felix
+1  A: 

Instead of using the NSIndexPaths tapped by the user I went with the underlying NSManagedObjects which is a lot safer (in case number or sorting of objects change) and faster (because I do not need the whole fetchRequest and or view).

I subclassed the UINavigationController and did the following.

When pushing a new TableViewController for a level (stored in parentLevel) I append this to an array in UserDefaults:

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
   [super pushViewController:viewController animated:animated];

   if([viewController isKindOfClass:[LevelTableViewController class]]){
       NSMutableArray *array = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:LevelTablesPersistentKey]];
       NSManagedObject *obj = [(LevelTableViewController*)viewController parentLevel];

       if(obj!=nil){
         [array addObject:[[obj objectID].URIRepresentation absoluteString]];
       } 

       [[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:array] objectForKey:LevelTablesPersistentKey];

   }
}

When I pop a viewController I simply remove the last entry from that array:

- (UIViewController *) popViewControllerAnimated:(BOOL)animated{
  UIViewController *vc = [super popViewControllerAnimated:animated];
  // remove last object
  if([vc isKindOfClass:[LevelTableViewController class]]){
     NSMutableArray *array = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:LevelTablesPersistentKey]];
     [array removeLastObject];
     [[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:array] objectForKey:LevelTablesPersistentKey];
  }

  return vc;
}

I can then use this array when initializing the NavigationController when the app is next started to rebuild the tree:

- (LevelNavigationController*) initWithRootViewController:(LevelTableViewController*)vc {
if(self = [super initWithRootViewController:vc]){
    // Recreate structure from UserDefaults
    NSArray *array = [NSArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:LevelTablesPersistentKey]];
    [[NSUserDefaults standardUserDefaults] setObject:nil forKey:LevelTablesPersistentKey]; // set the array to nil -> will be rebuild when pushing viewcontrollers onto navigation stack

    NSPersistentStoreCoordinator *persistentStoreCoordinator = ...; // pointer to coordinator 
            NSManagedObjectContext * managedObjectContext = ...; // pointer to your context
    for (NSString *objId in array) {
        NSManagedObjectID *mobjId=[persistentStoreCoordinator managedObjectIDForURIRepresentation:[NSURL URLWithString:objId]];
        if(mobjId!=nil){

            NSManagedObject *obj = nil;
            NSError **err = nil;
            obj = [managedObjectContext objectWithID:mobjId];

            if(err==nil && obj){
                if([obj.entity.name isEqualToString:@"Level"]){
                    // push level

                    LevelTableViewController *nextLevel = [[LevelTableViewController alloc] initWithStyle:UITableViewStylePlain];
                    nextLevel.parentLevel = (Level*)obj;
                    [self pushViewController:nextLevel animated:NO];
                    [nextLevel release];
                } 
            } 
        }
    }

}

return self;

}
Felix
Alternatively, you could set a property on each of your view controllers to hold the index/objectID for that view controller. Then, in `applicationWillTerminate:`, go through the array of view controllers from the `viewControllers` property of your navigation controller and add each to a mutable array. You would also need to load the array in `applicationDidFinishLaunching:` and then use it to restore your state. This has the advantage of not needing to subclass nor having to access the user defaults so often.
gerry3
I wanted to keep the appDelegate as clean as possible. As you correctly say: this comes at the expense of accessing the UserDefaults more often. I haven't seen any performance issues with that though. The subclassing works well for me as this table structure is just one of several tabs in a tabbarcontroller. One alternative would be to let the navigationController hold the array and save it on viewDidUnload. I'll take a look at that.
Felix