I am trying to create a Core Data app where the user is organizing a lot of information into sections. I have a main xib that has a popup menu and a non-bordered box. Into that box, a separate xib will be loaded with the view for the section chosen from the popup button.
I decided to make a second window/panel that's a sort of accessory window. The idea is that the main window shows a summary table, while the accessory view makes it easier to input data by taking the current selection in the summary table and displaying it in text fields, graphical date pickers (instead of forcing the user to use the correct format for typing a date into the table), etc. It also holds some optional fields and displays stats, so those don't clog up my main view.
My Document.m for the main xib has:
- (id)init
{
self = [super init];
if (self) {
viewControllers = [[NSMutableArray alloc] init];
accessoryViewControllers = [[NSMutableArray alloc] init];
ManagingViewController *vc;
ManagingViewController *accessoryVC;
vc = [[SummaryViewController alloc] init];
accessoryVC = [[SummaryAccessoryViewController alloc] init];
[vc setManagedObjectContext: [self managedObjectContext]];
[accessoryVC setManagedObjectContext: [self managedObjectContext]];
[viewControllers addObject: vc];
[accessoryViewControllers addObject: accessoryVC];
}
return self;
}
And so on for the other viewControllers/xib files that will be listed in the popup button. Making a selection in the popup returns its sender tag, then calls another method that takes the tag, and loads the objectAtIndex in the vc array into the main window box and accessoryVC array into the accessory window. In the actual SummaryViewController.m I have:
- (id) init {
self = [super initWithNibName: #"SummaryView" bundle:nil];
if (self) {
[self setTitle: #"Summary"];
}
return self;
}
I built all the views, then started binding. A column in the table in the main window might be bound to arrangedObjects.aaa and the accessory view's textfield will be bound to selection.aaa, but its selection won't change when the tableview selection changes. I'm guessing that's because technically they're using two separate NSArrayControllers.
I've seen examples in books where a secondary window had data synched to the main window, and it worked because both windows came from the same xib, so they used the same NSArrayController. My question is, which of these options can I use:
1) Is there a way to make the NSArrayControllers stay in synch across multiple xib files?
2) I could move the custom view in the SummaryAccessoryView.xib into SummaryView.xib so that the one xib contains both the view for the main and accessory windows. Then they would share NSArrayControllers. But then how do I get my popup to put one view in the main window and the other in the accessory window? My current method relies on [super initWithNibName: SummaryView.xib] so I don't see any way to specify which view.
3) I guess I could cave and rebuild the whole thing to a one-window model, scrap the redundant fields and put the extra fields at the bottom part of my main view, but the user won't be able to hide it or move it around and I have that issue again with having a user formatting their dates into a tableview... It might work if I knew how to have a graphical date picker come up when the user clicks a table cell. But I'd prefer to keep the two-window model if possible.
Any ideas on how to do option 1 or 2?
EDIT: I got option 3 working:
You need a few ivars first: a date picker (myDatePicker), your table (myTable), the pop-over that houses the date picker (myPopover), and the NSArrayController (myArray). Also in my example, the date column is the first column (column 0) and I've named it in IB as "date". If you have multiple dates (like start/end dates or two tables), you can add in an NSString ("tableAndColumn") that uses #define to set flags to identify which date you need, and turn your if statement into an if-else with multiple cases.
- (BOOL) tableView:(NSTableView *)tableView
shouldEditTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
if (tableColumn == [myTable tableColumnWithIdentifier: #"date"]) {
//tableAndColumn = myStartDate;
[myDatePicker setDateValue: [myArray valueForKeyPath: #"selection.date"]]; //this will set your date picker to the value already in the table column
NSRect rect = [myTable frameOfCellAtColumn: 0 row: [myTable selectedRow]];
[myPopover showRelativeToRect: rect ofView: myTable preferredEdge:NSMaxYEdge];
return NO;
// } else if (tableColumn == [myTable tableColumnWithIdentifier: #"endDate"]) {
// ...
} else {
return YES;
}
}
- (void) popoverWillClose:(NSNotification *)notification {
// if ([tableAndColumn isEqualToString: MyStartDate]) {
[myArray setValue: [myDatePicker dateValue] forKeyPath: #"selection.date"];
// } else if ([tableAndColumn isEqualToString: MyEndDate]) {
// ...
// }
}
You can bind to your array controllers across NIB files by using properties of your NIB file's owner that are key-value coding and key-value observing compliant. E.g. if one of your NIB files has your NSViewController subclass as the file's owner, you can bind controls to the file's owner using key paths that start with representedObject.
In your example, you could store your view controllers (which you initialized in -[Document.m init]) in dedicated properties, and set the NSViewController's representedObject to the document instance. Then, in your NIB file, you could bind your controls to the file's owner using a key path that starts with representedObject.myViewControllerProperty.myArrayControllerProperty etc.
In my own app, I initiate a custom window controller in -[Document makeWindowControllers] using -initWithWindowNibName and store it in a mainWC property. This main window controller creates subordinate view controllers (similar to how you've done it) and sets their representedObject property to itself ([vc setRepresentedObject:self]). All bindings in other NIB files are then routed thru this main window controller via bindings to file's owner using key paths that start with representedObject.
In a similar fashion, my MainMenu.xib file connects e.g. the "Enabled" property of some menu commands to appropriate array controller properties by binding to the Application object using key paths that start with mainWindow.windowController.document.mainWC.
Related
I'm trying to reload the values of a table view after exiting from a seque. The process being: I perform the seque manually from the profile selection view, add a new profile name, return to the profile selection view. Then I would like to reload the table view adding the new profile name. It is running the code fine (same code as original entry into the scene), but I can't seem to get the native methods of numberOfRowsInSection and numberOfRowsInSection to repopulate the table view. I actually have to leave the screen and reenter it before the new profile name will update. Any thoughts?
//** performing seque manually
-(IBAction)buttonAddNewProfile:(id)sender
{
// creating object for profile selection screen
UIStoryboard *ProfileSelectionStoryboard=[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
// creating object for add new profile storyboard
AddNewProfileViewController *addnewprofileVC=[ProfileSelectionStoryboard instantiateViewControllerWithIdentifier:#"Add New Profile"];
// setting the transition style
addnewprofileVC.modalTransitionStyle=UIModalTransitionStylePartialCurl;
// performing the segue
[self presentViewController:addnewprofileVC animated:YES completion:nil];
// performing new table view load on return from new profile
[self loadUsers];
}
//** function to load the new profile names in.
-(void)loadUsers
{
// retreiving the users from the database
SQLiteFunctions *sql = [[SQLiteFunctions alloc] init];
// testing for successful open
if([sql openDatabase:#"users"])
{
// setting query statement
const char *query = "SELECT * FROM users;";
// testing for that profile name existing already
if([sql getUserRecords:query] > 0)
{
// initializing array
NSMutableArray *names = [[NSMutableArray alloc] init];
// loop through object compling an array of user names
for(Users *ProfileUser in sql.returnData)
{
// adding user name to the listview array
[names addObject:ProfileUser.user_name];
}
// setting table view array to local array
tableData = names;
}
}
}
//** methods to reload the table view
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
{
// returning the number of rows in the table
return [tableData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
{
// setting up the table view cells for data population
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
// testing for cell parameters
if (cell == nil)
{
// setting up cloned cell parameters
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"MyCell"];
}
// setting cell values to the array row value
cell.textLabel.text = [tableData objectAtIndex:indexPath.row];
// returning the current row label value
return cell;
}
You have a few different options here:
1) The easiest is to simply reload the table every time that the view controller is about to display its view:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
}
The downside though, is that this will be executed every time that the view is displayed, even when you don't necessarily need to reload the data.
2) If you are using storyboard's and targeting iOS 6+ then you can use an unwind segue to call a specific method on your view controller when going back from the add profile view controller. For more info, see this SO question/answers: Does anyone know what the new Exit icon is used for when editing storyboards using Xcode 4.5?
3) If you are targeting older versions of iOS or aren't using storyboards, then you can create a protocol with a method that should be called whenever a new profile is added and you can reload the data whenever that method is called. There are lots of questions here on SO which cover how to do this (like dismissModalViewController AND pass data back which shows how to pass data, but you can do the same thing to just call a method).
This the actual answer from lnafzinger in the comments above. Thanks again.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
It took me a bit to figure this out, but it is because you are using
the UIModalTransitionStylePartialCurl modalTransitionStyle. I guess
they don't call it because it doesn't completely leave the screen. If
you use a different style (like the default) then it will call
viewWillAppear. If you want to stay with that, then you should
probably use one of the other methods. – lnafziger
I'm using an NSOutlineView with source list style, and using the view based (rather than cell based) outline view.
I would like to be able to make some rows bold. However, my attempts to change the font (manually in IB, through code in viewForTableColumn:…, or through the Font Bold binding) have so far been ignored.
From this message, it appears that this is because the source list style for NSOutlineView takes over managing the text field's appearance:
I'm guessing that you've hooked up your text field to the textField outlet of the NSTableCellView? If so, I think you might be running into NSTableView's automatic management of appearance for source lists.
Try disconnecting the text field from the textField outlet and see if your custom font sticks.
If I disconnect the textField outlet, the appearance does come under my control, and my emboldening works.
However, now I can't get it to look like the automatic one. By which I mean, when NSOutlineView was managing the text field's appearance, the font was bold and gained a drop shadow when any item was selected, but when I'm managing it manually this is not the case.
Can anyone answer either of these questions:
How can I get the Font Bold binding to work when NSOutlineView is managing the appearance of my text field
If I don't have NSOutlineView manage the appearance of my text field, how can I make it look and behave like it would if I did have it manage it?
I think I found the solution:
NSTableCellView manages the appearance of it's textField outlet by setting the backgroundStyle property on cells of contained controls. Setting this to NSBackgroundStyleDark triggers a special path in NSTextFieldCell which essentially sets an attributedStringValue, changing the text color and adding an shadow via NSShadowAttributeName.
What you could do is two things:
Set the backgroundStyle on your own in a custom row or cell view subclass.
Use a custom NSTextFieldCell in the cell's text field and change the behavior/drawing.
We did the latter since we needed a different look for a themed (differently colored) table view. The most convenient (albeit surely not most efficient) location we found for this was to override - drawInteriorWithFrame:inView: and modify the cell's attributed string before calling super, restoring the original afterwards:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSAttributedString *originalString = self.attributedStringValue;
// Customize string as you like
if (/* whatever */)
[self setAttributedStringValue: /* some string */];
// Regular drawing
[super drawInteriorWithFrame:cellFrame inView:controlView];
// Reset string
if (self.attributedStringValue != originalString)
self.attributedStringValue = originalString;
}
In the hope this may help others in similar situations.
Not sure if I have missed anything in your question but changing the font using the following works for me. ReminderTableCellView is just a subclass of NSTableCellView with an additional dateField added.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
//LOG(#"viewForTableColumn called");
// For the groups, we just return a regular text view.
if ([_topLevelItems containsObject:item]) {
//LOG(#" top level");
NSTableCellView *result = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
// Uppercase the string value, but don't set anything else. NSOutlineView automatically applies attributes as necessary
NSString *value = [item uppercaseString];
[result.textField setStringValue:value];
//[result.textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
return result;
} else {
//LOG(#" menu item");
// The cell is setup in IB. The textField and imageView outlets are properly setup.
// Special attributes are automatically applied by NSTableView/NSOutlineView for the source list
ReminderTableCellView *result = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
if ([item isKindOfClass:[OSTreeNode class]]) {
[result.textField setFont:[NSFont boldSystemFontOfSize:13]];
result.textField.stringValue = [item displayName];
result.dateField.stringValue = [item nextReminderDateAsString];
}
else
result.textField.stringValue = [item description];
if (_loading)
result.textField.textColor = [NSColor grayColor];
else
result.textField.textColor = [NSColor textColor];
NSImage *image = [NSImage imageNamed:#"ReminderMenuIcon.png"];
[image setSize:NSMakeSize(16,16)];
[result.imageView setImage:image];
//[result.imageView setImage:nil];
return result;
}
}
Resulting view is shown below. Note this is is an NSOutlineView with Source Listing option selected but I can't see why this would'nt work for a normal outlineView.
Hey I am working on an application that posts 50 location in a dynamic tableView and when you click on a location it will segue to a new tableView controller and posts 50 photos from that location. I created a tableViewController and then created a new file which contains all the files a tableView requires IE Cellforrowatindexpath. I have the segue connecting from the main tableViewcontroller but all the information is stored in the newfile which contains the methods that the tableViewController uses. Do I write PrepareForSegue in the tableViewController or do I write it in the file which has the methods that create the table? also if I write it in the tableViewCOntroller how do I access the cell name for one of the cells that are dynamically created? thanks.
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([segue.identifier isEqualToString:#"Photos for location"]){
//I dont know what to use for the name
[segue.destinationViewController setPhotoInPlace: WHAT DO I CALL THIS!?
}
}
The call names come from another file which uses public API to create an array of dictionaries which have information such as name and location. The file is called flickrFetcher. Here is code that dynamically creates the cells. self.brain is an instance of flickerFetcher and topPlaces is the method called from flickrFetcher to get the NSArray of NSdictionaries.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath
{
// Create an instance of the cell
UITableViewCell *cell;
cell = [self.tableView dequeueReusableCellWithIdentifier:#"Photo Description"];
if(!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"Photo Description"];
// set properties on the cell to prepare if for displaying
//top places returns an array of NSDictionairy objects, must get object at index and then object for key
// the cellTitle has country then province, country goes in main title and province will go as subtitle.
NSString * cellTitle = [[[[self.brain class] topPlaces] objectAtIndex:self.location] objectForKey:#"_content"];
NSRange cellRange = [cellTitle rangeOfString:#","];
NSString * cellMainTitle = [cellTitle substringToIndex:cellRange.location];
cell.textLabel.text = cellMainTitle;
NSString* cellSubtitle = [cellTitle substringFromIndex:cellRange.location +2];
cell.detailTextLabel.text = cellSubtitle;
//location is an int property that allows a new selection when using objectAtIndex
self.location++;
return cell;
}
prepareForSegue: is a UIViewController method, so it needs to be in your view controller. The sender parameter should be your cell that caused the segue. You can use a table view method indexPathForCell: to get the related index path and that should be enough to find the same data you put into the cell when you implemented cellForRowAtIndexPath:.
(I'm not sure what you mean by "a new file" or what class it implements, so I can't say if that affects anything.)
I'm currently trying to use a new view-based NSOutlineView in my Cocoa app. As I'm not using bindings, so I implemented all required delegate and datasource methods in my controller.
In interface builder I've added a NSOutlineView with a highlighting set to SourceList and Content Mode set to View Based. Thus, there were two default table cell views provided (one Header cell with HeaderCell set as identifier and one data cell with DataCell set as identifier)
This is what it looks like in interface builder, header cell views correctly show a grey-blue textField while data cell views have a image view and a textField with correct color and font settings
To provide the views, I use the following code, to return a DataCell-view or a HeaderCell-view and set the textField of the cell accordingly, based on the corresponding identifier set in interface builder.
- (NSView *)outlineView:(NSOutlineView *)outlineView
viewForTableColumn:(NSTableColumn *)tableColumn
item:(id)item {
NSTableCellView *result = nil;
if ([item isKindOfClass:[NSMutableDictionary class]]) {
result = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
id parentObject = [outlineView parentForItem:item] ? [outlineView parentForItem:item] : groupedRoster;
[[result textField] setStringValue:[[parentObject allKeys] objectAtIndex:0]];
} else {
result = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
[item nickname] ? [[result textField] setStringValue:[item nickname]] : [[result textField] setStringValue:[[item jid] bare]];
}
return result;
}
Running everything it looks like the following.
Could anybody provide me with hints, to why the header cell is neither bold, nor correctly colored, when selected?
You need to implement the -outlineView:isGroupItem: delegate method and return YES for your header rows. That will standardize the font and replace the disclosure triangle on the left with a Show/Hide button on the right. You will still need to manually uppercase your string to get the full effect.
I'm not sure if the group row delegate method above makes the selection style look okay or not. However, you normally don't want the header rows to be selectable at all in source lists, which you by returning NO for header items from the -outlineView:shouldSelectItem: delegate method.
I have created a little sample project which includes a source list and also uses the -outlineView:isGroupItem: method as #boaz-stuller has suggested.
Display a list of items
Edit the items in a master-detail fashion
Remove and add items
Usage of bindings
Check out besi/mac-quickies on github.
Most of the stuff is either done in IB or can be found in the AppDelegate
I am trying to create a preference pane which will reside within system preferences. All the bindings are done as usual (for a normal windowed application), but when the setter for the binding property is called (data is updated), the table data does not reset. Are preference panes capable of updating table data via bindings? I have also tried to use a table data source unsuccessfully.
To clarify, I have an NSMutableArray property in my prefPane's main class, an object representing the prefPane's main class, and an arrayController in IB which is bound to the table column. in the init method of the prefPane's main class, I set the value of the NSMutableArray, which is properly reflected in the pref pane, however, (just to test if bindings work), i have an NSTimer which resets the value of my NSMutable array when it finishes. A console message tells me that the value is properly reset, however, the changes are not reflected in the pref pane.
So in my current version i use the following code to set the properties to arbitrary values (simplified to try to get bindings to work at all). The property value is then reset by a timer 10 seconds later. Although the property is correctly updated (verified by console log), the pref pane does not reflect the changes in the tableview. Unfortunately, I cannot post screenshots of the bindings. I have an object in IB for the syncFrontEndPref class. I then have an arraycontroller bound to this object w/ a model key path of listArray. Then my table column is bound to the arraycontroller arranged objects. This loads properly with "test", "test1", "test2" in the pref pane (as populated from the init method). However, when repopulated from the timer, the changes are not reflected in the pref pane (although console log confirms listArray has indeed changed.
Here is the code:
#interface syncFrontEndPref : NSPreferencePane
{
NSMutableArray *listArray;
NSNumber *syncInt;
AuthenticateUser *newUser;
NSMutableArray *syncIntervalList;
IBOutlet NSTableView *theTableView;
}
#property (retain) NSMutableArray *listArray;
#property (retain) NSMutableArray *syncIntervalList;
- (void) mainViewDidLoad;
-(IBAction)syncIntervalValueChanged:(id)sender;
-(IBAction)tableViewSelected:(id)sender;
#implementation syncFrontEndPref
#synthesize listArray, syncIntervalList;
-(id) init{
//populate nsarray w/ list data to display
//[self setListArray: [NSMutableArray arrayWithArray:[[[NSDictionary dictionaryWithContentsOfFile:[GetFilePath pathForFile]] objectForKey:#"lists"] allObjects]]];
[self setListArray: [NSMutableArray arrayWithObjects: #"test", #"test1", #"test2", nil]];
//define values for drop-down sync interval selector
[self setSyncIntervalList:[NSMutableArray arrayWithObjects: #"1 minute", #"5 minutes", #"10 minutes", #"30 minutes", #"24 hours", nil]];
return self;
}
//code for the timer and selector method
- (void) mainViewDidLoad{
NSTimer *timer = [[NSTimer new] autorelease];
int syncTime = 10;
timer = [NSTimer scheduledTimerWithTimeInterval: syncTime target:self selector:#selector(targetMethod:) userInfo:nil repeats: YES];
}
-(void)targetMethod:(id)sender{
NSLog(#"running timer...");
[self setListArray: [NSMutableArray arrayWithObjects: #"0", #"1", #"2", nil]];
NSLog(#"%#", listArray);
}
I think you have two instances of your syncFrontEndPref object instantiated.
If you create a Preference Pane project from the template the File's Owner will be the NSPreferencePane. If you've added another entry for the syncFrontEndPref object, you will be creating a second copy of the object, and mainViewDidLoad won't be called in the second one. The timer won't be triggered for that copy of the object and the listArray won't be updated. Try adding a log statement to the init method. If you see that log statement run twice, you have two copies of the object.
If you do have two copies of the object, I'd suggest removing the copy you added to the xib in IB. Change the class of the File's Owner to your syncFrontEndPref class, and connect your bindings to that object.
Does this look something like your current xib file in IB?