How to share an NSManagedObjectContext between a NSPersistentDocument and view controller? - objective-c

New to OS X programming. Started with an Xcode template for a document-based app with Core Data.
In my default Document.xib I've created a View which I'm controlling with a custom ViewController. I then created a Managed Object Context in Document.xib and created two outlets, one to Document.h:
#property (strong) IBOutlet NSManagedObjectContext *myManagedObjectContext;
and one to ViewController.h:
#property (weak) IBOutlet NSManagedObjectContext *myManagedObjectContext;
In windowControllerDidLoad in Document.m, I then added self.myManagedObjectContext = [self managedObjectContext].
Following ghostfly's advice (could not locate an NSManagedObjectModel for entity name) I added:
NSLog(#"Context: %#",self.myManagedObjectContext);
NSLog(#"PS Coord : %#",self.myManagedObjectContext.persistentStoreCoordinator);
NSLog(#"MOM : %#", self.myManagedObjectContext.persistentStoreCoordinator.managedObjectModel);
NSLog(#"Entities : %#",[[self.myManagedObjectContext.persistentStoreCoordinator.managedObjectModel entities] valueForKey:#"name"]);
to my Document.m in windowControllerDidLoad and also in my ViewController's awakeFromNib. In Document.m everything seems to work fine: all the NSLog statements look right, and I can also add Entities into my NSManagedObjectContext but in the ViewController, only the first NSLog statement works, and the rest return (null).
My question: What's going wrong here, and am I even going about this the right way?
Various other questions seem to suggest either adding an AppDelegate to MainMenu.xib, but I'm not sure how this would work in practice in a Document-based application (for example, I'd expect each Document to have a separate NSManagedObjectContext, but if I use an AppDelegate, then surely they're all the same?), or even if that's recommended because some tutorials suggest this is explicitly not how to do it (e.g. here http://franck.verrot.fr/blog/2012/01/18/best-way-to-pass-nsmanagedobjectcontext-around-in-ios-applications/). Help is much appreciated! Thanks.

The above answer would create problems when you start to use multiple documents - it would not work as you would be saving just to the last opened document. You shouldn't try to create your own managed object context, because then you have to set up all the stuff for where to store the data etc (which is done already). It's much simpler:
Your main document class should be a subclass of NSPersistentDocument. If not, then you can literally just substitute that instead of NSDocument. The NSPersistentDocument has it's own managedObjectContext and persistentStoreCoordinator that you can use with in the document (to store document related data).
This therefore means each document has it's own managedObjectContext and persistentStoreCoordinator. To access these in other classes, you should pass the reference to the document. E.g. on your view controller subclass, create an #IBOutlet to the document, attach the document in interface builder and then in the constructor, copy the document's managedobjectcontext to it's own pointer in the view controller to use.

Edit: Correct answer is to use MagicalRecord (https://github.com/magicalpanda/magicalrecord) and save yourself a bunch of time!
My previous answer below:
Okay, I think I've got this working, but I'm not sure if this is the "best" or "most idiomatic" way of doing it. I'm going to spell it out in extreme detail in case someone else stumbles across this question and is equally stumped by how all this fits together...
First, forget about the ManagedObjectContext in the .xib; I'm not sure when it's appropriate to use that.
I created an application delegate called AppDelegate. In AppDelegate.h:
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property (retain) NSManagedObjectContext *managedObjectContext;
#property (retain) NSPersistentStoreCoordinator *persistentStoreCoordinator;
#end
Then, in MainMenu.xib, I drag in an NSObject and set its class to AppDelegate and connect the File's Owner's delegate to this object.
In Document.m, in the init method, I add:
AppDelegate *appDelegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
appDelegate.managedObjectContext = [self managedObjectContext];
appDelegate.persistentStoreCoordinator = [self managedObjectContext].persistentStoreCoordinator;
Note that I originally tried putting it in the windowControllerDidLoadNib method but this didn't work, because it seems like this is being called after my ViewController's awakeFromNib method.
Then, in my ViewController.m in awakeFromNib I can access the context by, e.g.:
AppDelegate *appDelegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
MyEntity *test = [NSEntityDescription insertNewObjectForEntityForName:#"MyEntity" inManagedObjectContext:[appDelegate managedObjectContext]];
I'll accept this answer if I don't find anything wrong with it over the next few days.
An open question is what will happen when I have multiple Documents open. Will they all share the same context?

Related

Why is this delegate method automatically called in Objective-C?

I'm going through this book called "cocoa programming for mac os x" and I just started with delegates. This whole thing with delegates is still a little bit wacky to me but I think I just need to let it settle.
However there was this one exercise where I should implement a delegate of the main window so that if resized height is always 2xwidth.
So I got 4 files:
AppDelegate.h
AppDelegate.m
WindowDelegate.h
WindowDelegate.m
AppDelegate are just the two standard files that get created when you open a new Cocoa project. I had to look up the solution because I didn't quite know how to accomplish this task.
The solution was just to create a new cocoa class, "WindowDelegat.h/.m" and add this to it's implementation file:
- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize {
NSSize newSize = frameSize;
newSize.height = newSize.width * 2;
return newSize;
}
Then I opened the interface builder, added a new object and made it my WindowDelegate. I then had to ctrl drag from the WindowDelegate to the actual window and made it the window's delegate.
Clicked run and it worked. Yay! But why?
First I thought that "windowWillResize" is just one of these callback functions that get's called as soon as the window is resized but it isn't. Normally methods get invoked because the general lifecycle of an program invokes them or because they are an #IBAction, a button or different control elements.
But "windowWillResize" is non of them. So why is it called?
EDIT: Problem solved! Thanks a lot!
Now I'm trying to connect the delegate to the window programmatically. Therefore I deleted the referencing outlet from WindowDelegate to the actual window in interface builder. It works but I just want to verify that this it the correct way how it's done:
AppDelegate.h
#import <Cocoa/Cocoa.h>
#import "WindowDelegate.h"
#interface AppDelegate : NSObject <NSApplicationDelegate>
#end
AppDelegate.m
#import "AppDelegate.h"
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (strong) WindowDelegate *winDeleg;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (void)awakeFromNib {
[_window setOpaque:NO];
NSColor *transparentColor = [NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.5];
[_window setBackgroundColor:transparentColor];
NSSize initialSize = NSMakeSize(100, 200);
[_window setContentSize:initialSize];
_winDeleg = [[WindowDelegate alloc] init];
[_window setDelegate: _winDeleg];
}
#end
WindowDelegate.h
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#interface WindowDelegate : NSObject <NSWindowDelegate>
#end
WindowDelegate.m
#import "WindowDelegate.h"
#implementation WindowDelegate
- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize {
NSSize newSize = frameSize;
newSize.height = newSize.width * 2;
return newSize;
}
- (id)init {
self = [super init];
return self;
}
#end
Why does the #property of WindowDelegate need to be strong?
And isn't my winDeleg an object? Why do I have to access it through _winDeleg when it's an object. I though the underscore is used to access variables?
Thank you for your help!
Clicked run and it worked. Yay! But why?
Because instances of NSWindow have a delegate property that can point to any object that implements the NSWindowDelegate protocol, and that protocol includes the -windowWillResize:toSize: method.
Read that a few times. The reason it's important is that you can create your own object, say that it implements NSWindowDelegate, implement -windowWillResize:toSize:, and set that object as your window's delegate. Then, whenever the user resizes the window, your method will be called and can modify the proposed new size.
Normally methods get invoked because the general lifecycle of an program invokes them or because they are an #IBAction, a button or different control elements. But "windowWillResize" is non of them. So why is it called?
This really isn't so different. Think of delegates as "helper objects." They let you customize the behavior of an object without having to create a whole new subclass. The NSWindowDelegate object is essentially a contract that the NSWindow promises to follow: whenever certain things happen, such as the user resizing the window, the window will call certain methods in its delegate object, if the delegate exists and implements those methods. In the case of NSApplication, a lot of those delegate methods are application lifecycle events, like the app starting up or quitting or getting a message from the operating system. In the case of NSWindow, delegate methods correspond to interesting events that can happen to a window, like the user moving it, hiding it, showing it, maximizing it, moving it to a different screen, etc. Other classes, like text views or network connections or movie players, have their own sets of interesting events and their own delegate protocols to match.
Note that methods marked IBAction really aren't delegate methods, they're just methods that get called by objects like controls that use a target/action paradigm. The IBAction keyword lets the IDE know which methods it should present as possible actions for things like buttons. You often find actions in window controllers and view controllers, and those objects frequently act as a delegate for some other object, but the actions themselves aren't part of the delegate protocol. For example, NSTableView takes a delegate object that determines how the table will act and what's displayed in it. It often makes sense for the view controller that manages the table to be the table's delegate, and that same view controller might also manage some buttons and contain the action methods that said buttons trigger, but the actions aren't part of the NSTableViewDelegate protocol and you therefore wouldn't call them delegate methods.

Changes not reflected across view when using binding in cocoa

I am creating some sample applications to understand the concepts of view navigation, binding etc in cocoa.
Here is the scenario:
I have a window that has a tab view(2 tabs) in MainMenu.Xib.
I have a text field in the first tab and label in the second tab. I want both of them to reflect the same value and I want to do this using binding. Also, I don't want to use the views provided to me along with the tab view.
These are the steps I have done.
The view of each tab view item is set separately in the applicationDidFinishLaunching: method using the following code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
//initialize view controllers
view1=[[ViewTab1 alloc] initWithNibName:#"ViewTab1" bundle:nil];
view2=[[ViewTab2 alloc] initWithNibName:#"ViewTab2" bundle:nil];
//set views
[[[myTabView tabViewItems] objectAtIndex:0]setView:view1.view];
[[[myTabView tabViewItems] objectAtIndex:1]setView:view2.view];
}
myTabView is the outlet reference of the tab view from MainMenu.xib in AppDelegate.
ViewTab1 is the name of the first view controller (and the xib).
ViewTab2 is the name of the second view controller (and the xib).
ViewTab1 has one single text field (and an associated label). I have bound this to a variable(name) declared in AppDelegate.
ViewTab2 has a label. I have bound this also to the same variable in AppDelegate.
The variable, 'name' is initialized in the init method of AppDelegate.
AppDelegate.h
....
NSString *name;
....
#property(strong) ViewTab1 *view1;
#property(strong) ViewTab2 *view2;
#property (assign) IBOutlet NSTabView *myTabView;
#property (strong) NSString *name;
....
AppDelegate.m
....
#synthesize myTabView;
#synthesize view1,view2;
#synthesize name;
....
- (id)init {
self = [super init];
if (self) {
name=#"dummy";
}
return self;
....
Apart from this I haven't done any coding in my program.
In the ViewTab1.xib I got an object and made it an instance of AppDelegate and then connected the delegate reference of the Application object(NSApplication) to the same object. (I hope this is the right way of getting the AppDelegate object.)
I did the same in ViewTab2.xib
Then I bound the text field in ViewTab1 and label in ViewTab2 to this variable in AppDelegate.
When I run the program both the text field and label shows "dummy". But when I change the value in the text field, its not reflected in the label in the second tab( i.e. ViewTab2).
Please tell me what I'm doing wrong.
How to establish binding to the same App delegate object from any loaded Nib?
Yes, I know this frustrated situation as described in question... after many weeks and hundreds pages of documentation for KVO - Notifications - Bindings I think there is one very simple solution for that.
As we can find in some information sources the nib-loading process produce new instances of members... and we need to use binding connection to the old one.
Note that bindings made in InterfaceBuilder are redirect to these new instances automatically after loading nib
Why not redirect the pointer of App delegate to the old instance?
In method where you loads your nib you can test which object is app delegate before and just after nib load.
If the new one isn’t the same as the previous one you can redirect it as you want.
This simple example works for me in Xcode3 under 10.5.8 with target to OSX10.5 / i386:
// ***** SOMEWHERE IN DEFAULT APP-DELEGATE.m IMPLEMENTATION
- (IBAction) createOtherWindowFromNib: (id)sender
{
// ensure that app delegate is set as you want...
[NSApp setDelegate:self];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE BEFORE NIB LOAD: %# ", [[NSApp delegate] description]);
// we can bind members of the nib to this controller over proxy object named "File’s Owner"
NSWindowController *otherWinCapo = [[NSWindowController alloc] initWithWindowNibName: #"OtherTestWindow"];
NSLog(#"APP-DELEGAT **** CREATE-TEST-WINDOW ***** WHO IS APP-DELEGATE AFTER NIB LOAD: %# ", [[NSApp delegate] description]);
// make some test for delegates before/after here if you need ...
// usually your bindings made inside "OtherTestWindow.xib" by IB doesn’t works in this moment
// ... and some redirection if needed
[NSApp setDelegate:self];
// afer that the bind made in IB inside "OtherTestWindow.xib"
// referred to (proxy object) "Application.delegate.myBOOL" (Bind to:Application, Model Key Path:delegate.myBOOL)
// react to changes of myBOOL placed in default app delegate object as expected
// simultaneously in every open instance of "OtherTestWindow.xib"
[otherWinCapo showWindow: otherWinCapo.window]; // we need populate the window instance on screen to see it
}
I think the problem is that the objects in your xibs that you set to the app delegate class create 2 different instances of the app delegate, so changing the value of the text field changes the value of name in one instance but not in the other. That's what you're doing wrong, unfortunately, I can't think of a solution at this time.
Have you turned on 'Continuously Updates Value' in the NSTextField controls?
See this example.

Xcode 4.2 & Storyboard, how to access data from another class?

I was wondering how I could access data from another class using Xcode 4.2 and Storyboard?
Say for instance how would I access the text of a text field from another class?
Google hasn't helped and the lesson on MyCodeTeacher.com about this is outdated and doesn't work anymore...
Thanks for bearing with me!
-Shredder2794
Not sure if this is the only or best way, but you can create a property in the destination view's .h file and set it to a value before the segue is performed
in the destination view controller's .h file:
#interface YourDestinationViewController : UIViewController
{
NSString* _stringToDisplay;
//...
}
#property (nonatomic, retain) NSString* stringToDisplay;
//...
and in the presenting view's .m file
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
YourDestinationViewController*viewController = segue.destinationViewController;
viewController.delegate = self;
viewController.stringToDisplay = #"this is the string";
}
Then you can do what you want with the property in whichever of the viewWillAppear/viewDidLoad/viewDidAppear/etc. methods best suits your purpose in the destination view's .m file
And then to check if it works, in the destination view controller's .m file:
-(void)viewWillAppear:(BOOL)animated
{
NSLog(#"self.stringToDisplay = %#", self.stringToDisplay);
...
//and if a label was defined as a property already you could set the
//label.text value here
}
Edit: Added more code, and made it less generic
This isn't specific to Storyboard. There are several ways to do what you are trying to do. You could declare a variable in your AppDelegate (an NSString) and set that in your first class. Then in your second class access the AppDelegate variable and use that to set your label. The code to do this is:
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
label.text = appDelegate.myString;
Another way to do it (probably the easiest) is to declare an NSString in your second class. Then in your first class, before you push the second view set that string variable. Something like this:
MyViewController *vc = [[MyViewController alloc] initWithNibName:#"" bundle:nil];
vc.myString = #"";
The third way to do this is using delegates. This is the most 'complicated' way but is the best. You would create a delegate which gets called when your second view appears. The delegate could then return the value from the first class to you.
You may also be able to use the new completion handler block on the iOS 5 pushViewController: method.
Edit:
Custom init method:
- (void)initWithNibName:(NSString *)nibName bundle:(NSString *)bundle string:(NSString *)myString
And then when you are pushing the view just class this method and set the string through it.

Xcode classes can't "see" one another

I was working on an Xcode project and everything was going great, until two of my classes stopped recognizing each other. Here is an excerpt:
#import "JBXViewController.h"
#interface ViewController2 : UIViewController {
JBXViewController *jbx;
}
For some reason I get the error "Unknown type name 'JBXViewController'; did you mean 'UIViewController'?" I don't understand how this is possible, since I'm importing the other class just a few lines above. Any insights would be much appreciated!
When you say they "can't see each other", I assume you mean you are also importing ViewController2.h in your JBXViewController.h. So you have one header importing another header, which imports the first header, which imports the second, which imports the first again...
Instead, use a forward reference to JBXViewController:
#class JBXViewController;
#interface ViewController2 : UIViewController {
JBXViewController *jbx;
}
And then #import "JBXViewController.h" in your implementation instead (in your ViewController2.m)
Firoze Lafeer's answer is correct, but this problem is probably a symptom of poor design in your code.
I assume that, in your app, JBXViewController is a parent view controller, and it sometimes shows a ViewController2 for some specific function. (For example, JBXViewController shows a list of records, while ViewController2 edits one of the records.) In a situation like this, ViewController2 should not know the details of JBXViewController. Instead JBXViewController should give ViewController2 the data it needs via ViewController2's properties, and if ViewController2 has to call methods on JBXViewController, they should be part of a delegate protocol.
For example, suppose JBXViewController currently has the following property, and ViewController2 accesses it:
#property (strong) JBXObject * currentObject;
You should instead have a currentObject property on ViewController2, and JBXViewController should set it before showing the view controller:
self.myViewController2.currentObject = self.currentObject;
[self.navigationController pushViewController:self.myViewController2 animated:YES];
This works for one-way communication—JBXViewController can give ViewController2 data. If data needs to flow back up to JBXViewController (other than by changing currentObject's properties), you should set up a delegate for ViewController2. For example:
#protocol ViewController2Delegate; // forward declaration
#interface ViewController2 : UIViewController
#property (weak) id <ViewController2Delegate> delegate;
...
#end
#protocol ViewController2Delegate <NSObject>
- (void)viewController2ShouldSave:(ViewController2*)viewController2;
- (BOOL)viewController2:(ViewController2*)viewController2 shouldAddSomoflange:(JBXSomoflange*)aSomoflange;
#end
Then have JBXViewController conform to the protocol:
#interface JBXViewController : UIViewController <ViewController2Delegate>
Set the delegate, either in Interface Builder or in code like so:
self.myViewController2.delegate = self;
self.myViewController2.currentObject = self.currentObject;
[self.navigationController pushViewController:self.myViewController2 animated:YES];
And implement all the methods listed in ViewController2Delegate.
Together, these changes mean three things:
ViewController2 does not need specific knowledge of how JBXViewController works. This means you no longer have to import JBXViewController.h in ViewController2.h, which solves your immediate problem.
JBXViewController is now more flexible. As long as it sets the appropriate properties in ViewController2 and implements any necessary delegate methods, you can change anything you want in JBXViewController and ViewController2 will never know or care about it.
ViewController2 is now more flexible too. You can use it from other parts of the app, or move it to another app. You can insert a screen between JBXViewController and ViewController2.
These changes aren't necessary to get the app running on your device and functioning the way you intend. But you'll have an easier time down the road if you start adopting these sorts of designs.

Objective C Delegate for the Main Application Window

So I'm trying to do this exercise where I need to set a delegate for the main window. The purpose is to make sure that when the user resizes the window, it's always twice as wide as it is high.
This is my AppController.h file:
#import <Cocoa/Cocoa.h>
#interface AppController : NSObject
{
NSWindow *windowWillResize;
}
#end
and this is my AppController.m file:
#import "AppController.h"
#implementation AppController
- (id) init
{
[super init];
windowWillResize = [[NSWindow alloc] init];
[windowWillResize setDelegate:self];
return self;
}
- (NSSize) windowWillResize:(NSWindow *)sender
toSize:(NSSize)frameSize;
{
NSLog(#"size is changing");
return frameSize;
}
#end
However, I can remove the line
[windowWillResize setDelegate:self];
since I set the delegate in Interface Builder, but I'm not sure why this works.
How does windowWillResize know that I'm referring to the main application window since I'm doing a completely new
windowWillResize = [[NSWindow alloc] init];
I have a feeling that I am completely doing this wrong. Could someone point me in the right direction? Thanks!
Indeed, you don't need to create a NSWindow *windowWilResize since a newly created Cocoa app already has a main window. You don't need to implement an -init method either.
You only need to set you appController as a delegate of your main window in Interface Builder and to implement the -windowWillResize: method in your appController.
If you are familiar with french language, you can take a look at a blog entry I have written on this subject: Délégation en Cocoa.
You're leaking an instance of NSWindow. In -init you create an NSWindow instance. However, that is not used because when the NIB loads, it sets up all the connections that you specified in Interface Builder and you start using the window from the NIB instead. Do not create a window object in code - Interface Builder does it for you! :-)
In fact, it's not quite "instead"; your app controller is now the delegate for both NSWindow instances - the one that comes from the NIB and the one you instantiated in -init. However as the in-code NSWindow is never used anywhere else, it's still redundant and should be removed.
If you just want to maintain the aspect ratio of the window you can use either of these two NSWindow methods:
setAspectRatio:(NSSize)
setContentAspectRatio:(NSSize)
The first method locks the entire window size, including the title bar. The second one just the content. You can call this method during the initialization of your window inside the delegate (for example: -applicationDidFinishLaunching)