I followed the advice here on how to setup a MainWindowController: NSWindowController
for my project's single window. I used a Cocoa class to create the .h/.m files, and I checked the option Also create .xib for User Interface
. As a result, Xcode automatically hooked up a window, which I renamed MainWindow.xib, to my MainWidowController.
Next, I deleted the window in the default MainMenu.xib file (in Interface Builder I selected the window icon, then I hit the delete key). After that, I was able to Build my project successfully, and my controller's window in MainWindow.xib
displayed correctly with a few buttons on it.
Then I tried adding an NSTableView to my MainWindowController's window. In Xcode, I dragged the requisite delegate
and datasource
outlets for the NSTableView onto File's Owner
, which is my MainWindowController, and I implemented the methods in MainWindowController.m that I thought would make the NSTableView display my data:
- tableView:viewForTableColumn:row:
- numberOfRowsInTableView:
Now, when I Build my project, I don't get any errors, but the data doesn't appear in the NSTableView.
My code is below. Any tips are welcome!
//
// AppDelegate.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@end
...
//
// AppDelegate.m
// TableViews1
//
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@property (strong) MainWindowController* mainWindowCtrl;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self setMainWindowCtrl:[[MainWindowController alloc] init] ];
[[self mainWindowCtrl] showWindow:nil];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
@end
...
//
// MainWindowController.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
@interface MainWindowController : NSWindowController
@end
...
//
// MainWindowController.m
// TableViews1
//
#import "MainWindowController.h"
#import "Employee.h"
@interface MainWindowController () <NSTableViewDataSource, NSTableViewDelegate>
@property (strong) NSMutableArray* employees;
@property (weak) IBOutlet NSTableView* tableView;
@end
@implementation MainWindowController
- (NSView*)tableView:(NSTableView *)tableView
viewForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
Employee* empl = [[self employees] objectAtIndex:row];
NSString* columnIdentifier = [tableColumn identifier];
//The column identifiers are "firstName" and "lastName", which match my property names.
//You set a column's identifier by repeatedly clicking on the TableView until only
//one of the columns is highlighted, then select the Identity Inspector and change the column's 'Identifier' field.
NSString* emplInfo = [empl valueForKey:columnIdentifier]; //Taking advantage of Key-Value coding
NSTableCellView *cellView =
[tableView makeViewWithIdentifier:columnIdentifier
owner:self];
NSLog(@"The Table view is asking for employee: %@", [empl firstName]);
[[cellView textField] setStringValue:emplInfo];
return cellView;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return [[self employees] count];
}
- (void)windowDidLoad {
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
Employee* e1 = [[Employee alloc] initWithFirstName:@"Joe" lastName:@"Blow"];
Employee* e2 = [[Employee alloc] initWithFirstName:@"Jane" lastName:@"Doe"];
[self setEmployees:[NSMutableArray arrayWithObjects:e1, e2, nil]];
//Test to see if the employees array was populated correctly:
Employee* e = [[self employees] objectAtIndex:0];
NSLog(@"Here is the first employee: %@", [e firstName]);
//I see the output: "Here is the first employee: Joe"
}
- (id)init {
return [super initWithWindowNibName:@"MainWindow"];
}
- (id)initWithWindowNibName:(NSString *)windowNibName {
NSLog(@"Clients cannot call -[%@ initWithWindowNibName] directly!",
[self class]
);
[self doesNotRecognizeSelector:_cmd];
return nil;
}
@end
...
//
// Employees.h
// TableViews1
#import <Foundation/Foundation.h>
@interface Employee : NSObject
@property NSString* firstName;
@property NSString* lastName;
- initWithFirstName:(NSString*)first lastName:(NSString*)last;
@end
...
//
// Employees.m
// TableViews1
//
#import "Employee.h"
@implementation Employee
- (id)initWithFirstName:(NSString *)first lastName:(NSString *)last {
if (self = [super init]) {
_firstName = first; //I read that you shouldn't use the accessors in init methods.
_lastName = last;
}
return self;
}
@end
File's Owner(=MainWindowController) connections:
NSTableView connections:
Response to comments:
Here is why calling [self tableView] reloadData]
at the end of -windowDidLoad
, as suggested in the comments, didn't work:
My _tableView
instance variable--created by my @property declaration in MainWindowController.m--doesn't point to anything; therefore calling:
[[self tableView] reloadData]
I think is equivalent to calling:
[nil reloadData]
which doesn't do anything.
I never assigned anything to the _tableView
instance variable in the -init
method, nor did I assign it a value by dragging an outlet somewhere in Interface Builder. To fix that problem, I selected MainWindow.xib (the controller's window) in the Project Navigator(left pane), and then in the middle pane(Interface Builder), I selected the cube representing the File's Owner(selecting the Identity Inspector in the right pane reveals that the File's Owner is the MainWindowController). Then in the right pane, I selected the Connections Inspector, and it revealed an outlet called tableView
, which is the IBOutlet variable I declared in MainWindowController.m.
Next, I dragged from the tableView outlet
onto the TableView in the middle pane:
Doing that assigns the NSTableView object to the _tableView instance variable that was created by my @property declaration in MyWindowControler.m:
@property (weak) IBOutlet NSTableView* tableView;
As an experiment, I disconnected the outlet, then commented out the @property declaration for tableview, and the tableView outlet no longer appeared in the Connections Inspector. Also, if I change the declaration from:
@property (weak) IBOutlet NSTableView* tableView;
to:
@property (weak) NSTableView* tableView;
...then the tableView outlet doesn't appear in the Connections Inspector. That experiment answered a couple of questions I had about whether I should declare a property as an IBOutlet or not: if you need to assign one of the objects in Interface Builder to one of your variables, then declare the variable as an IBOutlet.
Thereafter, calling [self tableView] reloadData]
at the end of -windowDidLoad
succeeds in populating the TableView. However, I have not seen any tutorials that call reloadData, and even Apple's guide does not do that.
So, I am still puzzled about whether calling -reloadData
is a hack or it's the correct way to do things.
Without it, your table view sits there blissfully clueless about your expectation that it should even bother asking its datasource for data.
I assumed that an NSTableView automatically queries its datasource when it is ready to display itself, and that my code needed to be able to provide the data at that time.
-windowDidLoad
. Since your data source has no data until-windowDidLoad
, the table gets no data. You change the data in your-windowDidLoad
. In general, the rule or a data source is that, whenever it changes the data it provides, it must tell the table view to reload. Some workarounds might include: initializing the data earlier, like in your initializer method; initialize the data on demand, so it's created the first time it's asked for, regardless of relative order with-windowDidLoad
. – Ken Thomases-numberOfRowsInTableView
? – 7stud-numberOfRowsInTableView:
and any other method that needs the data to have been initialized. The method would, of course, check if it had already been initialized and do nothing if so. – Ken Thomases