Working with the same NSManagedObjectContext in multiple tabs - iphone

I have a tab bar controller with different view controllers all using the same managed object context, being set up as follows:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
RootViewController *rootVC = [[RootViewController alloc] initWithStyle:UITableViewStyleGrouped];
rootViewController.managedObjectContext = self.managedObjectContext;
UINavigationController *rootNavCon = [[UINavigationController alloc] initWithRootViewController:rootVC];
[rootVC release];
SettingsTableViewController *settingsVC = [[SettingsTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
settingsVC.managedObjectContext = self.managedObjectContext;
UINavigationController *settingsNavCon = [[UINavigationController alloc] initWithRootViewController:settingsVC];
[settingsVC release];
tabBarController = [[UITabBarController alloc] init];
NSArray *controllers = [NSArray arrayWithObjects:rootNavCon, settingsNavCon, nil];
tabBarController.viewControllers = controllers;
[self.window addSubview:tabBarController.view];
[self.window makeKeyAndVisible];
return YES;
}
The idea is similar to the Recipes sample code if there were another tab called Settings which offered an option to managed the Category objects. The problem is that if you navigate to the view where the user can select the Category, but then go to the settings tab and delete, add, or edit a Category, when returning to the Recipes tab the changes will not be immediately reflected. Thus selecting a deleted Category would raise an exception.
What is the best way to deal with this? I was thinking about setting up an NSNotification to alert the views whenever an important change had occurred, but wasn't sure if there is a better way to do this, such as querying [managedObjectContext hasChanges] when a view appears. (Although that wouldn't seem to work if the context had already been saved.)

There's really no answer to your question other than "It's a design decision that you must make." Think carefully about your use cases and do the appropriate thing.
In your sample code, you are pushing a global managed object context into each controller. Each controller then does something with that managed object context, presumably. One thing that is not clear from the code is whether you are pushing additional view controllers onto the navigation stack or modally displaying other views. My assumption is that you are, and that any of the controllers and associated views can mutate a managed object.
In this situation, using a single managed object context might not be what you want when used with a tab view controller. Tabs allow the user to freely roam around the application so that he or she may see the appropriate view for whatever action he or she wants to perform. Allowing them to modify managed objects from a single context will potentially show changes in the wrong places, or cause an assumption to fail. It sounds like this is the situation you are in.
My advice to you would be to create multiple managed object contexts for each workflow that could mutate data. Then I would guess that you could share one managed object context for all read-only screens. Another alternative would be to use a managed object context dedicated for each tab. This would ensure that changes are made in isolation, and propagated out whenever necessary, but you will have to register the contexts to receive change notifications and then merge those changes as appropriate.
To illustrate, picture that the following diagram is an iPhone with multiple tabs, and each respective controller is handling some activity at given times.
Tab A: Controller M |--------------- ,------->
Controller N `-------+
| via NSNotificationCenter
v
Tab B: Controller O |-----------------------+-------->
Controllers M and O are members of the viewController array in the tab bar controller. The user starts on Tab A looks around, then navigates to Tab B. Then the user goes back to Tab A an pushes Controller N onto the navigation stack of Tab A and makes some change (like your your category deletion example). The deletion may or may not need to be propagated to Tab B. You can make this happen by having two managed object contexts where one listens for change notifications that the other has broadcasted via the notification manager.
Determining these use cases is probably the hard part, because I'm sure you understand how NSManagedObjectContext works in general (if not you should spend some time understanding it more thoroughly). As you can see from that simple diagram, for an extremely simple use case it introduces another dimension of complexity. So my point is that you must plan in advance if you don't want to have to tear down and reconstruct bits and pieces of your code base as it matures.

If you want changes to your managed object context to be propagated to your interface automatically and you're using table views (or even custom views), you could be using NSFetchedResultsController. This class watches a context for changes and triggers its delegate methods, allowing you to reload your views only when necessary.

Related

Switching view controllers without navigation controller

I'm sure this has been asked countless times, and I've seen similar questions though the answer still eludes me.
I have an application with multiple view controllers and as a good view controller does its own task. However I find myself stuck in that I can't switch from one view controller to another. I've seen many people say "use a navigation controller" but this isn't what I want to use due to the unwanted view elements that are part and parcel to view controller.
I've done the following and have had limited success. The view controller is switched but the view does not load and I get an empty view instead:
- (IBAction)showLogin:(id)sender
{
PPLoginViewController *login = [[PPLoginViewController alloc] initWithNibName:#"PPLoginViewController" bundle:nil];
PPAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = login;
[self.view insertSubview:login.view atIndex:0];
}
Using UINavigationController as a rootViewController is a good tone of creating iOS application.
As i understand unwanted view elements is a navigationBar? You can just hide it manually, setting:
[self.navigationController setNavigationBarHidden:YES];
And about your case, if you want to change you current viewController(targeting iOS 6), you can just present new one:
[self presentViewController:login animated:YES completion:nil];
or add child (Here is nice example to add and remove a child):
[self addChildViewController:login];
Why to set UINavigationController as a root?
1) First of all it makes your application visible viewcontrollers to be well structured. (Especially it is needed on iPhone). You can always get the stack and pop (or move) to any viewController you want.
2) Why I make always make navigation as a root one, because it makes the application more supportable, so to it will cost not so many code changes to add some features to the app.
If you create one (root) viewcontroller with a lot of children, or which presents other viewcontrolls, it will make your code really difficult to support, and make something like gode-object.
Listen to George, UINavigationController is the way to go. Your reasons for not wanting to use it are not valid.
However, the reason your code doesn't work might have to do with the unnecessary line after setting the rootViewController to the login vc.
Per Apple's documentation, setting rootViewController automatically sets the window's view to the view controller's view.

Refreshing the content of TabView

Ok I am trying to refresh the tab content of each of my tabs after a web call has been made, and I have tried soo many different methods to do this that I have lost count. Could someone please tell me how this is possible?
The web call just calls JSON from a server and uses it to update the content of the tabs. For testing purposes I have a button set up inside my settings class. Settings class is a view within the home tab which has a button called refresh. When clicked this takes JSON stored on the device which is different to the one called from the web call on application start up. This saves me having to change the JSON on the server.
I will take you through some of the techniques I have tried and would be grateful if someone could tell me what I am doing wrong.
I tried making an instance of the class and calling the refresh method like this
DashboardVC *db = [[DashboardVC alloc] init];
[db refreshMe];
The refresh method in dashboard class is this
-(void) refreshMe
{
[self loadView];
[self viewDidLoad];
}
However no luck. This method will work if I call it inside the Dashboard class, but wont work if I call it from another class. I think it is become I am instantiating a new class and calling refresh on that. So I dropped that technique and moved onto the next method
This loops through all the tabBars and changes the tabTitles without any issues, so it I know it is definitely looping through the ViewControllers properly.
I also tried every varient of the view methods like ViewDidAppear, viewWillAppear etc, no luck.
I also tried accessing the refreshMe method I made in the dashBoard class through the tabController like this
[[[self.tabBarController viewControllers] objectAtIndex:0] refreshMe];
But again no luck, this just causes my application to crash.
I read through this guide
https://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/ViewControllerPGforiOSLegacy/TabBarControllers/TabBarControllers.html
on the apple website but it doesn't seem to cover how to refresh individual tab content.
All I want is to have each individual tab refresh its content after the web call is made, and have spent ages trying to figure this out, but nothing is working.
So would be very grateful if someone could show me what I am doing wrong?
Thanx in advance....
EDIT:
Expand on what I have tried
After discussion with Michael I realised you should never call loadView as against Apple guidelines. So I removed any references to LoadView. I have now placed a method in all the main ViewControllers called RefreshMe which sets up the views, images texts etc in the class. And this method is placed inside the ViewDidLoad. Now I want to be able to call these methods after a web call has taken place, so effectively refreshing the application.
My viewDidLoad now looks like this in all my the main classes.
- (void) viewDidLoad {
[super viewDidLoad];
[self refreshMe];
}
And then the refreshMe method contains the code which sets up the screen.
The JSON data pulled from the web call will set up the content of each of the 5 tabs, so need them all to be refreshed after web call.
I tried looping through the viewControllers and calling viewDidLoad, which should in turn call the refreshMe method which sets up the class, however nothing happens. Code I used was this
NSArray * tabBarViewControllers = [self.tabBarController viewControllers];
for(UIViewController * viewController in tabBarViewControllers)
{
[viewController viewDidLoad];
}
For the time being I have also included
NSLog(#"Method called");
in the viewDidLoad of each class to test if it is being called. However the message is only being printed out when I first load the application or if I re-enter the application. This method should be called after I click the refresh button in the settings screen but it isn't and I have no idea why.
Anyone have any idea why this is not working?
From the question and your comments, it sounds like there are at least two problems:
You're having trouble accessing the view controllers managed by your app's tab bar controller.
You seem to be working against the normal operation of your view controllers.
The first part should be straightforward to sort out. If you have a pointer to an object, you can send messages to that object. If the corresponding method doesn't execute, then either the pointer doesn't point where you think it does or the object doesn't have the method that you think it does. Let's look at your code:
NSArray * tabBarViewControllers = [self.tabBarController viewControllers];
for(UIViewController * viewController in tabBarViewControllers)
{
[viewController viewDidLoad];
}
This code is supposed to call -viewDidLoad on each of the view controllers managed by some tab bar controller. Leaving aside the wisdom of doing that for a moment, we can say that this code should work as expected if self.tabBarController points to the object that you think it does. You don't say where this code exists in your app -- is it part of your app delegate, part of one of the view controllers managed by the tab bar controller in question, or somewhere else? Use the debugger to step through the code. After the first line, does tabBarViewControllers contain an array of view controllers? Is the number of view controllers correct, and are they of the expected types? If the -viewDidLoad methods for your view controllers aren't being called, it's a good bet that the answer is "no," so figure out why self.tabBarController isn't what you think.
Now, it's definitely worth pointing out (as Michael did) that you shouldn't be calling -viewDidLoad in the first place. The view controller will send that method to itself after it has created its view (either loaded it from a .xib/storyboard file or created it programmatically). If you call -viewDidLoad yourself, it'll either run before the view has been created or it'll run a second time, and neither of those is helpful.
Also, it doesn't make much sense to try to "refresh" each view controller's view preemptively. If your app is retrieving some data from a web service (or anywhere else), it should use the resulting data to update its model, i.e. the data objects that the app manages. When a view controller is selected, the tab bar controller will present its view and the view controller's -viewWillAppear method will be called just before the view is displayed. Use that method to grab the data you need from the model and update the view. Doing it this way, you know that:
the view controller's view will have already been created
the data displayed in the view will be up to date, even if one of the other view controllers modified the data
you'll never spend time updating views that the user may never look at
Similarly, if the user can make any changes to the displayed data, you should ensure that you update the model either when the changes are made or else in your view controller's -viewWillDisappear method so that the next view controller will have correct data to work with.
Instead of refreshing your view controllers when updating your tab bar ordering, why not simply refresh your views right before they will appear by implementing your subclassed UIViewController's viewWillAppear: method?
What this means is that each time your view is about to appear, you can update the view for new & updated content.

Programmatically navigating in iOS

I'm in the process of porting an existing Android app to iOS, and I'm pretty inexperienced in this environment. The examples I've read so far for navigating multiple views all use some kind of visual user control for triggering the loading and unloading of views (tab bar, nav bar). This application's navigation needs to be pretty strict and not allow the user to freely move around between the three views.
The app needs to have a full screen splash view, a main view that the user interacts with, and a third view for data collection. The splash screen should appear, and the user should be navigated to the main view when tapping on the splash image. There will be custom logic in the main view controller for determining if data is required at which point it should navigate to the data collection view. Once valid data is entered and the user clicks OK, it should navigate back to the main view.
What I've read so far is that all Views should have an associated UIViewController class, and the easy way to do this is to create the XIB and UIViewController class in one shot and link them together (I have plenty examples/books/tutorials that I can reference for that part). I believe what I've read is that the app should have a root UIViewController that handles loading the others and navigating between them.
My questions are:
What class should I derive from for my main view controller that I use to load the others?
How do I wire that up to the app so that it knows to load this as the main controller?
What is the accepted standard way of having a navigation controller in the app and allowing the other views to obtain a reference to it? Should my UIViewControllers hold a reference to their parent controller, or should they ask the UIApplication for a reference to it when needed? How do I make sure I don't instantiate extra copies of the views and their controllers as the user navigates?
What class should I derive from for my
main view controller that I use to
load the others?
UIViewController
How do I wire that up to the app so
that it knows to load this as the main
controller?
Read the "Defining Your Subclass" section of View Controller Programming Guide for iOS. Scratch that -- read the whole thing. It's all important, you might as well start to learn it now. Also read App Programming Guide for iOS. Again, read the whole thing, but the application lifecycle part is the most relevant to your question.
What is the accepted standard way of
having a navigation controller in the
app and allowing the other views to
obtain a reference to it?
Again, this is explained well in View Controller Programming Guide. Views should never care about the navigation controller, but any view controllers that are part of a navigation stack have direct access to the nav controller via their respective navigationController properties.
Should my UIViewControllers hold a
reference to their parent controller,
or should they ask the UIApplication
for a reference to it when needed?
A view controller already has a reference to its parent controller in its (surprise!) parentController property. It's best for a controller to avoid assuming too much about its
parent, though. If the controller expects its parent to be a certain type or respond to certain messages, it becomes more difficult to reuse that controller or reorganize your application. Try to give the controller what it needs to do its thing when you create it. If the controller will need to ask for additional data or something like that, delegation is a good way to go.
How do I make sure I don't instantiate
extra copies of the views and their
controllers as the user navigates?
Exercise caution. There's not much danger of creating extra copies of views in a properly structured application because each view controller should take care of its own views. If you find yourself loading or otherwise creating views outside the context of the view controller that owns them, stop that.
It sounds like you can accomplish what you need with a couple of basic calls. To programmatically call a view controller:
- (void)showController {
MyViewController *myController = [[MyViewController alloc] initWithNibName:#"MyViewControllerXIB" bundle:nil];
[self.navigationController pushViewController:myController animated:YES];
[myController release];
}
To return to the previous view just call from any view controller:
- (void)goBack {
[self.navigationController popViewControllerAnimated:YES];
}
Read up on the documentation for the UINavigationController for more ways to move through views. This method is just one of many ways to do this and may not be suitable for all situations.
Not quite right — each UIViewController should know how to trigger its children. Apple's preferred navigation path through views is a branching tree, with the caveat of tab bars that collapse multiple view controllers into a single node on the tree.
You don't explicitly handle loading. Normally you have a sufficient relationship between your NIBs that the container classes are loaded automatically. Cocoa will then load the views whenever they're needed but not yet loaded (which is the purpose of loadView and viewDidLoad), and keep them unless and until a low memory warning requires them to be purged (leading to viewDidUnload). It's relatively rare that you explicitly load a NIB yourself (though table view cells are an obvious example where programmatically loading a NIB is quite common).
So you'd probably have:
a splash screen or preview of the first view controller, as the Default.png
a view controller that probably displays Default.png, and has two outlets going to the data collection controller and the main controller
when the user taps the button on the main screen, ask the model whether data collection is necessary. If so then navigate to the data collection controller, otherwise navigate to the main controller
give the data collection controller an outlet to the main controller and let it perform a navigation there at the appropriate moment
You get a MainWindow.xib for free when creating a new view based project. Probably the easiest thing to do is to put references to the three UIViewController subclasses in there, but set each of them to load from other files. Set the links between them in MainWindow.xib, set the links to things within the relevant views within the relevant XIBs.
That will prevent you from keeping multiple instances of any controllers about, and the built-in Cocoa loading mechanisms will ensure that the stuff that occupies significant amounts of memory — the views — is loaded only on demand and kept for no longer than space allows.
There's no need to link to parent view controllers. Every view controller already knows who presented it, via the parentViewController property. So if a view controller wants to dismiss itself and return to whoever presented it, you can just issue:
[self.parentViewController dismissModalViewControllerAnimated:YES];
Because the model is ideally a separate sovereign thing, all controllers really need to know is which other controllers they can present, how to populate themselves from the model and how to push data back to the model. You rarely end up with particularly complicated links between view controllers.
I think you should have the view loading/unloading in the application delegate and then each view should send notifications to the application delegate.
Here is the official introduction from Apple:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Notifications/Introduction/introNotifications.html
You can set before which ViewController to load first
If the application is navigation based use the following code:
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MainViewController *mainViewController = [[MainViewController alloc] initWithNibName:#"MainViewController" bundle:nil];
self.nav = [[UINavigationController alloc] initWithRootViewController:mainViewController];
[_window addSubview:nav.view];
[_window makeKeyAndVisible];
}
If the application is View based use the following code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.viewController = [[ViewController alloc] initWithNibName:#"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}

why does this code use presentModalViewController? (not pushViewController)

Anyone understand why in the CoreDataBooks example code that:
(a) method for controller swapping difference
Whilst the click an item and go to detailed view uses what seems to be the standard UINavigationController concept of "pushViewController", that when when you click on the "Add" a new record button it launches the new view to add the record via "presentModalViewController" approach? That is, couldn't the approach have been the same in both cases, just using a pushViewController approach?
Are there actually any advantages to using each approach for where it's been used? I can't quite see. I'd guess there must have been something for Apple to choose these different approaches for different scenarios. For example:
any differences to the user (i.e.
UI differences or functional
differences) that they would see?
any differences for the developer
(or advantages/disadvantages)
For example, if you were to consider using pushViewController approach instead of the presentModalViewController approach for the for the "Add" scenario...
(b) data sharing approach difference
the approach to how they share the common data object seems to be different - so again just wondering why the approaches weren't the same? (i.e. in both cases the main controller is passing off to another view temporarily and there is some shared data between them - i.e. that the child view needs to pass back to the parent)
Code Extract for Convenience
That is for "Edit":
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Create and push a detail view controller.
DetailViewController *detailViewController = [[DetailViewController alloc] initWithStyle:UITableViewStyleGrouped];
Book *selectedBook = (Book *)[[self fetchedResultsController] objectAtIndexPath:indexPath];
// Pass the selected book to the new view controller.
detailViewController.book = selectedBook;
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
}
But for "Add"
- (IBAction)addBook {
AddViewController *addViewController = [[AddViewController alloc] initWithStyle:UITableViewStyleGrouped];
addViewController.delegate = self;
// Create a new managed object context for the new book -- set its persistent store coordinator to the same as that from the fetched results controller's context.
NSManagedObjectContext *addingContext = [[NSManagedObjectContext alloc] init];
self.addingManagedObjectContext = addingContext;
[addingContext release];
[addingManagedObjectContext setPersistentStoreCoordinator:[[fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
addViewController.book = (Book *)[NSEntityDescription insertNewObjectForEntityForName:#"Book" inManagedObjectContext:addingContext];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:addViewController];
[self.navigationController presentModalViewController:navController animated:YES];
[addViewController release];
[navController release];
}
thanks
You use modal view controllers to focus the user's attention on a Task. When you push, the user is in some kind of navigation flow, but still has the total application at their fingertips. They might decide to go forward or backward, switch to a different tab in the middle, whatever. When they get a modal view controller, they can't do any of that until the task is completed or canceled out of (the modal view is dismissed)
[Warning: this answer applies more to the updated code of the CoreDataBooks, which has changed to use the new-in-iOS5 setParentContext method of NSManagedObjectContext instead of messing with the persistentStoreCoordinator[
Your 2nd question about data sharing is also answered by the modal Add vs modeless Edit approach. Run the app in the simulator and notice that:
if you click on Add your next view has both Save and Cancel buttons
if you click on Edit your next view has only a Done button.
(Now, in this particular project, you have to edit each field at a time and the field editing is done in yet another view, and that one has a Cancel button, but ignore that for now, because
a. this only applies to the field. E.g. If you edit a Title and hit Save, you're back at the Edit view with the Done button, now there's no cancel to undo that change, you can only hit Done. As far as this view is concern, you've edited the Book modeLESSly
b. What a lame UI! Come on Apple, make the CoreDataBooks into a decent, albeit simple app that follows your own conventions. At least put the editing in the cells.)
Where were we? Oh yeah, "Edit"-ing an existing Book is modeLESS, so it passes the original Book in the same MOC (NSManagedObjectContext) and you can't cancel your edits to it in the Edit view.
"Add"-ing a Book, on the other hand is MODAL: It creates a new Book for to be edited in the detail view, and wants to discard it if the user hits cancel. To achieve this it has to use a second MOC, which is a child of the first. If the user Cancels, it simply ignores the new child MOC, effectively discarding the new Book; if the user Saves, it saves the child MOC, which pushes the new Book and its properties up into the parent MOC, then saves the parent MOC.
This child-MOC approach, btw, is detailed in the WWDC 2011 presentation 303 "What's new in Core Data on iOS".
There are other approaches discussed elsewhere in SO, including
Creating a new managed object with a nil MOC, and only inserting it in the parent MOC when the user hits save
Not using a managed object but a different data structure for the temporary object (the new Book that we're not sure we want to save yet), such as an NSDictionary, or just a set of different variables
and more... ?
I kind of prefer the parent-child approach because Apple favours it and because it makes use of the data model objects instead of creating parallel data structures for temporary objects.
The nil-context approach also has that benefit, and the added benefit of (apparently) better performance and simplicity (read my lips: no new MOCs). But I'm not convinced that managed objects without managed object contexts are kosher.
By the way, CoreDataBooks doesn't exactly follow the convention laid down in the aforementioned presentation, because it doesn't save the parent context in a performBlock block.
Also I'm not sure why it sets the new managed context as a property on the AddViewController and doesnt use it.

How do I access the managedObjectContext from a controller deep in the UI?

I'm still a little fuzzy on understanding iPhone/Cocoa in general so this is probably a simple question.
I have a CoreData Window-Based App for the iPhone. The rootController is a UITabBarController. The first tab view has a UINavigationController attached to it with a table in it's main view.
When the App starts the objectContext is set up, which makes sense to have the App do that once. But now I have the managedObjectContext in the main Controller but I want to get that passed down to the Controller of the View inside the navcontroller, inside the first item in the TabBarController's tab list. How do I do this?
Would naming the one of the fields in the UI Inspector Tool allow me to do something like:
tabcontroller.navcontroller.manageObjectContext = self.managedObjectContext;
Would this only work if the controller was instantiated and 'live'. (Do the controllers not get instantiated until they are needed?) What if this was in a view that was for the most part hidden?
Anyway this is probably a simple thing I'm just not understanding things properly yet.
What is the general right way to share the manageObjectContext that is created and setup in the rootController to the many sub-controllers in the app?
I'm guessing this is the preferred method assuming the core-data initialization is done in the AppDelegate:
[[[UIApplication sharedApplication] delegate] managedObjectContext]
I usually give controllers a - (id)initWithManagedObjectContext:(NSManagedObjectContext *)context init method and a corresponding variable.
If a controller creates another controller in turn, it will, if needed, pass the NSManagedObjectContext to that controller in the same manner.
If you don't want to create an extra init method, just give the controllers a property for the NSManagedObjectContext and set that property directly after creating them.
I usually try to limit the number of controllers that directly deal with and "know about" Core Data though.
The answers to this question provide several means of accessing the Core Data stack deep within your application. As I indicate in one of the comments, I prefer to use a singleton DatabaseController that can be accessed from wherever, similar to how the NSUserDefaults' standardUserDefaults works.