[Objective C] Document Controller and Window Controller

Everything related to programming.
User avatar
Sparky
Delta Force
Posts: 4194
Joined: Wed Mar 31, 2004 8:59 pm
Location: New Jersey, USA
Contact:

[Objective C] Document Controller and Window Controller

Post by Sparky » Fri May 10, 2013 2:52 am

NSDocumentController - class reference
NSWindowController - class reference

When you generate a new document-based application in Xcode, you have one document controller that Xcode sets up in a template. This single NSDocumentController object is designated the File's Owner for file handling with every kind of document type used in the program -- some people subclass it and call it AppDelegate, other people call it PrimaryInterfaceController, whatever. You subclass NSDocument for every kind of file the program reads and writes, but you only subclass NSDocumentController once. The single document controller is used to manage these specific NSDocument file types, and each NSDocument subclass can have several windows associated with it. The NSDocumentController also manages the main menu in your application.

Each window that an NSDocument subclass uses gets a subclass of NSWindowController. Unlike the more singular NSDocumentController, of which the application uses only one subclass regardless of how many subclasses of NSDocument it has, each different kind of window gets a different subclass of NSWindowController. This difference is because NSDocumentController is used for the main menu bar as the File's Owner (but in practice we use our own subclass object instead as explained later) and to manage the different kinds of documents, while the specific document types are customized within each NSDocument subclass, and each NSDocument has separate NSWindowControllers for each type of window the document uses, and the NSWindowControllers become the File's Owner's to their respective windows. So you would customize a single subclass of NSDocumentController for the main menu to work with the NSDocument subclasses, and you would customize NSWindowController for each unique type of NSWindow subclass. Your customization for the actual functionality of each document goes into that document type's subclass of NSDocument. Each NSDocument class manages its own NSWindowController, and each NSWindowController manages its own window(s) of a specific appearance and use.

The management hierarchy is as follows:
NSDocumentController (one subclass customized for main menu options and serving each kind of new document type) -- one, pre-included with document-based application template in Xcode, our subclassed object is File's Owner of main menu
NSDocument (one NSDocument subclass per document type, customized within the class) -- one per document type
NSWindowController (customized to link the document with its window view using IBOutlets and such) -- for each NSDocument subclass, one per window type, File's Owner's of their respective NSWindow's
NSWindow (what the user sees on the screen) -- one per window


How documents work with window controllers:

// The document manages the file i/o and has its own window controller, and the window controller manages the window. So the NSWindowController subclass #import's the NSDocument subclass for displaying the file data in the window, and the NSDocument subclass #import's its NSWindowController subclass to receive the new values that the user entered in the window. The file manipulation and variables are in the document controller, while the window manipulation and outlets are in the window controller. The two send messages to each other to indicate variables' values: document to window for displaying, and window to document for setting. So for READING files, file parsing is in the NSDocument, the NSDocument sends the parsed data in an array to the NSWindowController, and the NSWindowController presents those values in the window. For WRITING files, or setting values before saving them to the file, the NSWindowController sends each modified value by key-pair to the NSDocument, which matches it to its array and later saves the array (in re-parsed format)to the file through the File -> Save menu item.

You can add windows to a document in the .xib / .nib customizer (typically still called Interface Builder) and ...here is what you do:

~ Start a new document-based application in Xcode.
~ In the left navigation sidebar, add folders called Cocoa Interfaces, Document Classes, and Controllers. If your program uses a multitude of document types, categorize them with one or more subfolders within Cocoa Interfaces, Document Classes, and Controllers.
~ Click on the Controllers folder in the left sidebar, go to File -> New -> New File... and choose Objective-C class. Subclass NSDocumentController. Call it PrimaryInterfaceController.
~ Click on the Document Classes folder in the left sidebar, use File -> New -> New File... and add one Objective-C subclass of NSDocument for each document type that the application will use, naming them recognizably. When you add each new document type subclass, Xcode will also add a respective .xib interface file inside the Cocoa Interfaces folder, which you can move into one of your subfolders to keep larger projects organized.
~ In the Cocoa Interfaces folder, click on the MainMenu.xib file, and you should see an interface editor. At the top of the right side panel, click on the Identity Inspector and at the bottom, show the Object Library and type NSObject in to the search field below. Drag a blue Object cube from there to the left panel of your interface editor view, so it is below the blue cube object for Font Manager. With the new object selected, look back at the top right Identity Inspector and for the Custom Class "class" field, type in PrimaryInterfaceController.
~ You can now edit your Main Menu interface, such as by adding a selection menu for each new type of document your application uses as File -> New -> (list of document menu items). When you're done customizing your Main Menu interface, you will come back to this later to connect your IBAction's to the menu items.
~ Edit PrimaryInterfaceController.h, which is your subclass of NSDocumentController. #Import each header file of each NSDocument subclass found in your Document Classes folder.

Code: Select all

#import "DocumentTypeSubclassA.h"
#import "DocumentTypeSubclassB.h"
#import "DocumentTypeSubclassC.h"
#import "DocumentTypeSubclassD.h"
Then, after

Code: Select all

@interface PrimaryInterfaceController : NSDocumentController {
@private
    
}
but before @end, add IBAction method declarations for each custom menu item, such as for your "File -> New ->" document initialization methods:

Code: Select all

- (IBAction)newDocumentTypeA:(id)sender;
- (IBAction)newDocumentTypeB:(id)sender;
- (IBAction)newDocumentTypeC:(id)sender;
- (IBAction)newDocumentTypeD:(id)sender;
~ Edit PrimaryInterfaceController.m. Scroll down to the "@implementation PrimaryInterfaceController" section and add new methods for each method declaration added in the header file. Your IBAction menu item method will, when its menu item is chosen, do the following actions: instantiate a new object pointer of your respective NSDocument subclass and set it equal to (remember, you are editing the NSDocumentController subclass presently) calling an NSDocumentController method "makeUntitledDocumentOfType: error: " on "self" (which is PrimaryInterfaceController), so that the NSDocumentController initializes the document and its windows from its .nix for you; after this, if the document object exists, we use "addDocument:" on "self" to add that document to the list of open documents handled by PrimaryInterfaceController (our NSDocumentController subclass); then we send the document messages to "makeWindowControllers", "showWindows", and whatever other window tweaking we need through one or more other methods we can add to that NSDocument subclass and its NSWindowController(s) to manipulate the windows. So add something like this to PrimaryInterfaceController.m's @implementation section:

Code: Select all

- (IBAction)newDocumentTypeA:(id)sender
{
	DocumentTypeSubclassA *docA1 = [self makeUntitledDocumentOfType:@"com.company.johndoe.file-doctypeA" error:NULL];
	if (docA1 != nil)
	{
		[self addDocument:docA1];
		NSLog(@"'addDocument:' method completed.");
		[docA1 makeWindowControllers];
		NSLog(@"'makeWindowControllers' method completed.");
		[docA1 showWindows];
		NSLog(@"'showWindows' method completed.");
		[docA1 adjustWindow];
		NSLog(@"'adjustWindow' method completed.");
	}
}

- (IBAction)newDocumentTypeB:(id)sender
{
	DocumentTypeSubclassB *docB1 = [self makeUntitledDocumentOfType:@"com.company.johndoe.file-doctypeB" error:NULL];
	if (docB1 != nil)
	{
		[self addDocument:docB1];
		[docB1 makeWindowControllers];
		[docB1 showWindows];
	}
}

- (IBAction)newDocumentTypeC:(id)sender
{
	DocumentTypeSubclassC *docC1 = [self makeUntitledDocumentOfType:@"com.company.johndoe.file-doctypeC" error:NULL];
	if (docC1 != nil)
	{
		[self addDocument:docC1];
		[docC1 makeWindowControllers];
		[docC1 showWindows];
	}
}

- (IBAction)newDocumentTypeD:(id)sender
{
	DocumentTypeSubclassD *docD1 = [self makeUntitledDocumentOfType:@"com.company.johndoe.file-doctypeD" error:NULL];
	if (docD1 != nil)
	{
		[self addDocument:docD1];
		[docD1 makeWindowControllers];
		[docD1 showWindows];
	}
}
In the above code, you will need to add some unique UTI (uniform type identifier) to recognize your file type, which you also set up in your project by clicking on your project in the left navigation menu and clicking on your application under "Targets" in the left menu, then adding one Document Type and Exported (if it's coined by your program) or Imported (if it's used by other programs too) UTI for each type of document your application uses. For each Document Type entry, set its name, related NSDocument subclass, extensions, and Identifier (UTI). The Identifier (UTI) in the Document Type entry must match its Exported/Imported UTI entry also. For information about how to use the "Conforms To" field in the UTI entries, see Apple's Online documentation of the Universal Type Identifier. In all three types of entries, you can also associate an icon image you've put together for that file type, if available.

Anyway, the UTI in your code, such as in the above example appearing as "com.company.johndoe.file-doctypeA" is used to reference the Document Type and Exported/Imported UTI entries which you specified as in the previous paragraph and are stored in your application's <app>-Info.plist file found inside the Supporting Files subfolder.

I add some NSLog entries along the way for debugging purposes and to clarify the program logic flow.

adjustWindow is a custom method example for manhandling the windows or their views when they load. I use it to adjust the scrollbar positions inside embedded NSScrollView views, so they start at a certain position by default. More on that later.

You can delete the lines for NSLog if you like, and for adjustWindow if you don't find you need to do any window adjustments when the document window opens. I left the A document type more involved, but it can be as streamlined as B, C and D in the above code.

~ Edit the MainMenu.xib file again. Navigate to each menu item entry for each File -> New -> document type menu item, right-click (or control-click) on the menu item entry and drag the resulting blue line to the blue object cube in the left tray for PrimaryInterfaceController, then select from the popup menu "newDocumentTypeA" or whatever method you have associated for starting the new document, as listed under Received Actions. This links the interface menu item with the IBAction for starting that new document. Do this for each document type your program supports.

The main menu for your application is now customized for use with the NSDocumentController subclass. We won't touch the NSDocumentController subclass anymore in this tutorial, but you might like to adjust it some more for saving files or for other main menu document-related options. We now deal with the NSDocument subclasses and their related NSWindowController subclasses.

~ Click on the Controllers folder in the left sidebar, go to File -> New -> New File... and choose Objective-C class. Subclass NSWindowController. Name it similarly to the NSDocument subclass with which it will be used, such as WC_DocumentTypeSubclassA (where WC stands for Window Controller). Do this for each window that each NSDocument subclass will use, so one NSWindowController per window inside each document, naming appropriately.
~ Edit the WindowController subclass header files to add #import statements for their associated NSDocument subclass header files, such as:
in WC_DocumentTypeSubclassA.h, add to the top:

Code: Select all

#import "DocumentTypeSubclassA.h"
We'll come back to this later to add IBOutlets that the windows will have that the WindowController will use to display in the window the data sent to it by its related NSDocument subclass.
~ Edit the NSDocument subclass header file. #Import its window controller subclass, declare any custom variables, window adjustment methods and whatnot. (Yes, the window adjustment methods could go in the window controller subclass instead, but we don't do that in this example, even though it would be better to do so. You'll see how to adjust this after you familiarize yourself with this tutorial; I'll try to remember to mention it later also.)

Code: Select all

#import "WC_DocumentTypeSubclassA.h"
~ Edit the NSDocument subclass main file, such as "DocumentTypeSubclassA.m". Define a new "makeWindowControllers" method:

Code: Select all

- (void)makeWindowControllers
{
		NSLog(@"Make Window Controllers reached.");
		// the document manages the file i/o and has its own window controller, and the window controller manages the window
	WC_DocumentTypeSubclassA *doctypeA_WC1 = [[WC_DocumentTypeSubclassA alloc] init];
		NSLog(@"Window Controller: %@", doctypeA_WC1);
	[self addWindowController: doctypeA_WC1];
		NSLog(@"Window Controllers: %@", [self windowControllers]);
}
If I could give you one tip about objective-c programming, I'd say to remember which subclass file you are editing when you use the "self" keyword. In the code block above, "self" refers to the NSDocument subclass, so we can call NSDocument methods on it in addition to any other methods we have added to our NSDocument subclass. We allocate and initialize a window controller and add it to the NSDocument subclass's list of its window controllers, since it keeps track of the window controllers and not the windows themselves; NSWindowController subclasses keep track of their own windows.

An adjustWindow method, defined in your NSDocument or NSWindowController subclass header file(s), might look like this in the .m file for NSDocument subclass DocumentTypeSubclassA.m (it would be much more simplified if located in WC_DocumentTypeSubclassA.m instead, but again, you can adjust this blemish later):
usable in DocumentTypeSubclassA.m, but can be simplified if placed in WC_DocumentTypeSubclassA.m instead, which it really should be (you can do this yourself once you've figured out this tutorial)

Code: Select all

- (void)adjustWindow
{
	NSLog(@"Reached the 'adjustWindow' method.");
	NSEnumerator *e = [[self windowControllers] objectEnumerator];
	WC_DocumentTypeSubclassA *wc = [[self windowControllers] objectAtIndex:0];
	while (wc == [e nextObject])
	{
			//NSWindow *window = [wc window];
		NSLog(@"Window Controller for adjustments: %@", wc);
		[wc adjustWindowScroll];
	}
		//NSWindowController *wc = [[self windowControllers] objectAtIndex:0];
	NSLog(@"Window Controller for adjustments: %@", wc);
	
}
Again, since "self" here refers to the NSDocument subclass and windowControllers called on it returns the list of window controllers it has, you can probably just use "[self objectAtIndex:0]" if you place the code into your NSWindowController subclass instead.

You might not see all the NSLog items in the above code block, but it should still function properly.

~ Edit again the window controller for your NSDocument subclasses' main windows. In WC_DocumentTypeSubclassA.h,
add after

Code: Select all

@interface WC_DocumentTypeSubclassA : NSWindowController {
the IBOutlets for every text field and embedded subview (such as NSTabView or NSScrollView) that you will be referencing. For the adjustment method, I defined outlets for the NSScrollView's I was adjusting with adjustWindowScroll:

Code: Select all

IBOutlet NSScrollView *WindowSV;
That scroll view outlet was used to reference the main window's scroll view, but take heed that referencing embedded views is impossibly difficult, even if you spent hours like I did trying to get them to work --- instead, define them like above and edit like follows:
~ Edit your NSDocument subclass's .nix / .nib file. First, click in the left tray on File's Owner. In the Identity Inspector on the right, set the class to your window controller, for example, WC_DocumentTypeSubclassA.
~ Now edit the window as you see fit, as you expect the document data to appear in it. You can do this programmatically, but for now, use the interface editor. When you are done editing the window's contents and any subviews you also defined as IBOutlets like in the previous steps, continue.
~ Use the top menu of the interface editor to navigate to any subviews you would like to define as IBOutlets and any other text fields or whatnot. For each window interface element, we'll right-click/control-click and drag from the File's Owner cube to the element and assign it an IBOutlet (subviews and text fields) or IBAction (buttons and "action" items) that we declared in our window controller subclass header file. So for each IBOutlet you added previously to your NSWindowController subclass, link it to the .nix interface file in this way.
~ Edit your window controller's main file, such as WC_DocumentTypeSubclassA.m. If it exists, comment out this entire method body:

Code: Select all

- (id)initWithWindow:(NSWindow *)window
And add this one instead:

Code: Select all

- (id)init
{
	self = [super initWithWindowNibName:@"DocumentTypeSubclassA" owner:self];
	return self;
}
replacing the NSString value with the name of your NSDocument subclass's .nib (.xib) file, which should be named after your NSDocument subclass. This lets the NSWindowController parent class initialize your NSWindowController subclass with that .nib / .xib file and assigns your window controller subclass as the .nib / .xib file's owner.

Now if you wanted to put the window adjustment method inside your WindowController subclass, you would add the tasks inside this method:

Code: Select all

- (void)windowDidLoad
{
    [super windowDidLoad];
    
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
}
so that the adjustment steps happen after the window loads. But just as a point of curiosity and potential usefulness, adjusting the scrollbars looks like this method in the way we use it presently, as I have used it:

Code: Select all

- (void)adjustWindowScroll
{
	NSLog(@"Reached the 'adjustWindowScroll' method.");
		// Reposition window scroll view bars to top
	NSLog(@"We're going to attempt to reposition the Flags area scroll view to the top now.");
	NSLog(@"Window Controller: %@", self);
	NSWindow *MainWindow = [self window];
	if (MainWindow) {
		NSLog(@"Main window was recognized: %@", MainWindow);
	}
	else
	{
		NSLog(@"Main window was NOT recognized.");
	}
	NSLog(@"Attempting first adjustment");

// here is your nutcase attempt at trying to reference embedded subviews without defined IBOutlets... it doesn't work right!
//	NSClipView *sv = [[[[[[[[MainWindow contentView] scrollView] documentView] tabView] tabViewItemAtIndex:0] view] scrollView] contentView];
// so we use the IBOutlets for subviews instead, as described previously in this tutorial:

	[[FlagsSV contentView] scrollToPoint:(CGPoint){0,336}];
	[[MoreFlagsSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PerceptionSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[MovementSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[LookingSV contentView] scrollToPoint:(CGPoint){0,868}];
	[[UnopposableSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PanicSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[DefensiveSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PursuitSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[BerserkSV contentView] scrollToPoint:(CGPoint){0,109}];
	[[FiringPositionsSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[CommunicationSV contentView] scrollToPoint:(CGPoint){0,0}];
// unfortunately, you will probably need to experiment and tweak the CGPoint values manually, unless you are doing this more programmatically, since the scrolled view sizes are not the same as the sizes in Xcode's interface editor (points rather than pixels)

	NSLog(@"Attempting second adjustment");

/* another crazy attempt here which does not work:
	[[[[[[[MainWindow contentView] documentView] tabView] tabViewItemAtIndex:0] view] scrollView] reflectScrolledClipView: [[[[[[[MainWindow contentView] documentView] tabView] tabViewItemAtIndex:0] view] scrollView] contentView]];
		//[scrollView reflectScrolledClipView: [self contentView]];
*/
// this does work:
	[FlagsSV reflectScrolledClipView: [FlagsSV contentView]];
	[MoreFlagsSV reflectScrolledClipView: [MoreFlagsSV contentView]];
	[PerceptionSV reflectScrolledClipView: [PerceptionSV contentView]];
	[MovementSV reflectScrolledClipView: [MovementSV contentView]];
	[LookingSV reflectScrolledClipView: [LookingSV contentView]];
	[UnopposableSV reflectScrolledClipView: [UnopposableSV contentView]];
	[PanicSV reflectScrolledClipView: [PanicSV contentView]];
	[DefensiveSV reflectScrolledClipView: [DefensiveSV contentView]];
	[PursuitSV reflectScrolledClipView: [PursuitSV contentView]];
	[BerserkSV reflectScrolledClipView: [BerserkSV contentView]];
	[FiringPositionsSV reflectScrolledClipView: [FiringPositionsSV contentView]];
	[CommunicationSV reflectScrolledClipView: [CommunicationSV contentView]];

	NSLog(@"Adjustments successful");
}
----

Now I didn't go through this lengthy tutorial myself with a new application, but I think I covered all the steps. If you find a problem, please let me know.
EDIT: I did move the adjustments to the window controller subclass for the window. It should be evident what I did in the source code to Zeus. Instead of posting a revised version of this tutorial, I'll just say for now that you simply remove the call to showWindows from the PrimaryInterfaceController and paste that call as [self showWindows] at the end of your makeWindowControllers method in your NSDocument subclass. You can put it there because it's standard that when you make the window controller, you can show its window and bring it to the front. Then you also cut/paste the method for adjusting the window scroll from the NSDocument subclass and into its NSWindowController subclass, simplifying it into two methods, one broad for all the adjustments (even though there is just one, but in case you add other adjustments later) and one specific for the scrolling adjustment, as such:

window controller header file has

Code: Select all

- (void) adjustWindow;
- (void) adjustWindowScroll;
and window controller main file has

Code: Select all

- (void)windowDidLoad
{
    [super windowDidLoad];
    
    // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
	[self adjustWindow];
}

- (void)adjustWindow
{
	NSLog(@"Reached the 'adjustWindow' method.");
	NSLog(@"Window Controller for adjustments: %@", self);
	[self adjustWindowScroll];	
}

- (void)adjustWindowScroll
{
	NSLog(@"Reached the 'adjustWindowScroll' method.");
		// Reposition window scroll view bars to top
	NSLog(@"We're going to attempt to reposition the Flags area scroll view to the top now.");
	NSLog(@"Window Controller: %@", self);
	NSWindow *MainWindow = [self window];
	if (MainWindow) {
		NSLog(@"Main window was recognized: %@", MainWindow);
	}
	else
	{
		NSLog(@"Main window was NOT recognized.");
	}
	NSLog(@"Attempting first adjustment");

	[[FlagsSV contentView] scrollToPoint:(CGPoint){0,336}];
	[[MoreFlagsSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PerceptionSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[MovementSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[LookingSV contentView] scrollToPoint:(CGPoint){0,868}];
	[[UnopposableSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PanicSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[DefensiveSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[PursuitSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[BerserkSV contentView] scrollToPoint:(CGPoint){0,109}];
	[[FiringPositionsSV contentView] scrollToPoint:(CGPoint){0,0}];
	[[CommunicationSV contentView] scrollToPoint:(CGPoint){0,0}];
	
	NSLog(@"Attempting second adjustment");

	[FlagsSV reflectScrolledClipView: [FlagsSV contentView]];
	[MoreFlagsSV reflectScrolledClipView: [MoreFlagsSV contentView]];
	[PerceptionSV reflectScrolledClipView: [PerceptionSV contentView]];
	[MovementSV reflectScrolledClipView: [MovementSV contentView]];
	[LookingSV reflectScrolledClipView: [LookingSV contentView]];
	[UnopposableSV reflectScrolledClipView: [UnopposableSV contentView]];
	[PanicSV reflectScrolledClipView: [PanicSV contentView]];
	[DefensiveSV reflectScrolledClipView: [DefensiveSV contentView]];
	[PursuitSV reflectScrolledClipView: [PursuitSV contentView]];
	[BerserkSV reflectScrolledClipView: [BerserkSV contentView]];
	[FiringPositionsSV reflectScrolledClipView: [FiringPositionsSV contentView]];
	[CommunicationSV reflectScrolledClipView: [CommunicationSV contentView]];

	NSLog(@"Adjustments successful");
}
Either you are groping for answers, or you are asking God and listening to Jesus.

Who is online

Users browsing this forum: No registered users and 1 guest