Is an NSValueTransform subclass a good choice for displaying Core Data attributes into UI views displaying:
A number string like (0,1,2,3,etc) into a string such as (Pending, Completed, Frozen, In progress, etc)
A number string like (0,1) into a app-based image (red.png if 0, green.png if 1)
Here's what Core Data displays for the two attributes, timer and status:
Here is what I want to be displayed instead, without changing the values in Core Data:
If not to use NSValueTransformer, in what other way is this possible?
I do not want to see the data permanently converted, only for the benefit of less data stored in Core Data and better UI view items.
I have also tried to modify the attributes in the managed object class (with out KVO notification) with no luck.
Yes, NSValueTransformer subclasses work just fine for this purpose.
You can also add read-only computed properties to your managed object class, and that should work, too. Those properties can even be added by a category in the controller code, if they don't make sense as part of the model code.
For example:
+ (NSSet*) keyPathsForValuesAffectingStatusDisplayName
{
return [NSSet setWithObject:#"status"];
}
- (NSString*) statusDisplayName
{
NSString* status = self.status;
if ([status isEqualToString:#"0"])
return #"Pending";
else if ([status isEqualToString:#"1"])
return #"Completed";
// ...
}
The +keyPathsForValuesAffectingStatusDisplayName method lets KVO and bindings know that when status changes, so does this new statusDisplayName property. See the docs for +keyPathsForValuesAffectingValueForKey: to learn how that works.
I ended up using what at first appeared to be blocking the display of different info in those cells, using:
#pragma mark - Table View Delegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
/* tableColumn = (string) #"AutomaticTableColumnIdentifier.0"
row = (int) 0 */
NSString *identifier = [tableColumn identifier];
NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
NSManagedObject *item = [self.itemArrayController.arrangedObjects objectAtIndex:row];
if ([identifier isEqualToString:#"AutomaticTableColumnIdentifier.0"]) {
/* subviews returns array with 0 = Image View &
1 = Text Field */
/* First, set the correct timer image */
... logic ...
NSImageView *theImage = (NSImageView *)[[cellView subviews] objectAtIndex:0];
theImage.image = [NSImage imageNamed:#"green.gif"];
/* Second, display the desired status */
NSTextField *theTextField = (NSTextField *)[[result subviews] objectAtIndex:1];
... logic ...
theTextField.stringValue = #"Pending";
}
return cellView;
}
Apple's documentation states (somewhere) that bindings with an Array Controller can work in combination with manually populating the table view cells. It seems best and easiest to start with bindings and then refine display values manually.
Related
So I've been looking for an answer to this interesting issue I came across but haven't had very much luck. Basically I have a UITableView preloaded on initial app launch with a few objects using CoreData, and the ability for the user to add more.
I allow deleting of cells in the table, except for the items I have initial pre-loaded. So I perform a check in my canEditRowAtIndexPath method and return NO if the selected item is one of these pre-loaded items. Everything works great until I scroll down far enough for one of the items to be offscreen, and then when it snaps back the item that shouldn't be editable, is now editable.
I'm fairly new to iOS development, so I'm hoping this is a rather amateur issue - but I can't seem to find the answer.
Any help is appreciated.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
DataLayer *dl = [DataLayer alloc];
// Get all items from Favorites
NSArray *results = [dl FetchAll:#"Favorites"];
// Get currently selected cell properties
FavoritesTableCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
Favorites *fav = [Favorites alloc];
NSMutableArray *genItems = [[NSMutableArray alloc] init];
// Get only records that are default app items
for(int a = 0; a < [results count]; a++){
fav = [results objectAtIndex:a];
if ([fav.generic isEqualToNumber:[NSNumber numberWithInt:1]]) {
[genItems addObject:fav];
}
}
// Loop through default items to determine if editable
for (int i = 0; i < [genItems count]; i++) {
fav = [genItems objectAtIndex:i];
if ([fav.title isEqualToString:[selectedCell.nameLabel text]]) {
return NO;
}
}
return YES;
}
The root of the problem is that this method is basing it's answer on the content of a table view cell (selectedCell) rather than the model.
Table views reuse cells. As they are scrolled off the view, the "new" cells that appear are really the same object's that just disappeared on the other side of the table. So that selectedCell is not a good reference for a question that ought to be put to your model.
The code needs to be structured like this:
Your model is a NSMutableArray that starts with a few items you add. You need to know which items are originals, not to be removed:
#property (nonatomic, strong) NSMutableArray *favorites;
#property (nonatomic, assign) NSMutableArray *genericFavorites;
// at init, imagine these strings are your preloaded core data
[self.genericFavorites addObject:#"generic favorite object a"];
[self.genericFavorites addObject:#"generic favorite object b"];
[self.favorites addItemsFromArray:self.genericFavorites];
You'll use self.favorites as your model, that is when table view asks numberOfRowsInSection, you'll answer self.favorites.count. In cellForRowAtIndexPath, you'll lookup the item in self.favorites[indexPath.row] and configure the cell with data from that object. self.genericFavorites just helps you remember which objects are original, not added by the user.
If the order remains fixed, then your canEditRow logic is simple:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
return indexPath.row >= self.genericFavorites.count;
}
But if, as maybe your code implies, the user can reorder these items, then your canEditRow has more work to do, but it can do that work without reference to the table cells (which as I indicated, are unreliable):
// get the object at indexPath.row from our model. Answer NO if that object was preloaded
// from the app (if genericFavorites containsObject:), YES otherwise
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
Favorite *favorite = self.favorites[indexPath.row]; // this is your model after user has edited, reordered, etc
return ![self.genericFavorites containsObject:favorite];
}
I have a NSTableview and a button. NSTableview has a unknown number of columns.
The first column has a image well and 2 text boxes, the others (again, unknown number) are normal textbox columns.
The button opens up a file open dialogue. Once I choose the files (.jpg) I would like to process them.
So far everything is made (chose files, columns, etc..) what is missing is the populating of the table:
I have the loop that goes through all the selected files. What is the best way to do this:
display the image in the image well of the first cell,
type the filename in the first textbox of the first cell,
type the filepath in the second cell of the textbox,
type "YES" in all other columns.
My difficulty is that I have no idea how many columns will be there since it depends from the user. The number of columns will not change during Runtime. they are set up at startup based on the configuration. if the configuration is changed then the app should be reloaded.
I am a beginner in Objective-C/Cocoa programming.
EDIT:
additional info as requested:
It is a view based NSTableView
each column represents an action that has to be taken in a later moment on an image. the program user can decide what and how many actions to take, thats the reason for a unknown amount of columns in the table view.
You can add columns programmatically using addTableColumn:. This method takes an NSTableColumn instance that you can create in your code.
The rest of your architecture (displaying images, etc.) does not particularly change from "normal" code just because the columns have been added dynamically.
Here is a snippet that should get you started:
NSTableColumn* tc = [[NSTableColumn alloc] init];
NSString *columnIdentifier = #"NewCol"; // Make a distinct one for each column
NSString *columnHeader = #"New Column"; // Or whatever you want to show the user
[[tc headerCell ] setStringValue: columnHeader];
tc.identifier = columnIdentifier;
// You may need this one, too, to get it to show.
self.dataTableview.headerView.needsDisplay = YES;
When populating the table, and assuming that the model is an array (in self.model) of NSDictionary objects, it could go something like this;
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSString *columnIdentifier = tableColumn.identifier;
NSDictionary *rowDict = [self.model objectAtIndex: row];
NSString *value = [rowDict valueForKey: columnIdentifier]; // Presuming the value is stored as a string
// Show the value in the view
}
More in the docs.
When user adds a column or row, you should reflect it in your model (by binding or by code), so you know the size of your table, when you need to populating it.
set tableView.delegate (in code or in Interface Builder), reference here
implement:
- (NSView*) tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row
{
Item* itemView = [tableView makeViewWithIdentifier:#"rowItem" owner:self];
/*Here you populate your cell view*/
id entryObject = [self.entries objectAtIndex:row];
[itemView setEntry:entryObject];
return itemView;
}
and then invoke [tableView reloadData];
maybe for you better to use this method
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
Just see the NSTableViewDataSource and NSTableViewDelegate
I have a tableview with dynamical amount of cells, based on an array who is build up by a JSON.
tableData = [NSJSONSerialization JSONObjectWithData:dataWebService options:kNilOptions error:&error];
This little line gets all the data I need to build my tableview.
In the tableview I have a textfield in every cell with predefined text from my JSON. The user can change this value before he proceed to the next segue (storyboard).
The question is how I can store this usertyped values in an array an replace the excisting values in my tableData array before I send this arraobject to the next segue.
And keep in mind that the user input must not change when the user scroll in the tableview.
Maybe this is basic, but I cannot figure a nice way to do this.
This is an example of the JSON.
(
{
defaultval = 8;
dtype = long;
id = "#franr";
label = "Faktura nr.";
},
{
defaultval = 8;
dtype = long;
id = "#tilnr";
label = "Til faktura";
}
The tableview look like this:
(I cannot post images)
Fra faktura 8
Til faktura 8
The user can change the values (number 8) to any numbers. So I need to replace the numbers of defaultVal in my JSON array.
Thanks.
Since its a UITEXTFIELD, you can use OnTextChange Event
[textField addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
or
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
, or lose focus
- (BOOL)textFieldShouldReturn:(UITextField *)textField
please keep in mind you need to set the delegate of the textfield to self & add the respective protocals.
In order to keep the data after scrolling, you should modify the data source (the array) after each input from the user. You can do dis in the UITextFieldDelegate's textFieldDidEndEditing: method (of course, you have to be the delegate for each text field: textField.delegate = self;).
You should also implement a UIScrollViewDelegate method, for dismissing the keyboard when the table begins to scroll (e.g. scrollViewWillBeginDragging:).
EDIT:
Your array must be containing NSDictionaries, which are not mutable, so I recommend, you create your own class having those attributes (defaultval, dtype ...), so you can edit the data source. I don't think you can modify your array the way it is now.
The code with custom object would be something like:
- (void)textFieldDidEndEditing:(UITextField *)textField {
UITableViewCell *cell = textField.superview.superview;
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
MyObject *currentObject = [tableData objectAtIndex:indexPath.row];
currentObject.defaultVal = textField.text.intValue;
}
I have a simple NSTableView which I have loaded with data from a NSMutableArray. When I select a row (entry) in the tableView and modify it, I can find the row index, what I cannot get is the edited entry out as a string so I can modify the array. I can find lots of information on selecting rows, etc., but not on how to get the actual modified string. I keep thinking this should be real simple. Help please.
Part of my code:
- (IBAction)toDoEdit:(id)sender // Accept the edited data
{
NSString *toDoItem = [[toDoTableCell:toDoTableView dataCellFoTableColumn:0 row:rowToBeEdited] stringValue];
// I get the error "dataCellForTableColumn' method cannot be found.
[toDoArray replaceObjectAtIndex:rowToBeDeleted withObject:toDoItem];
[toDoTableView reloadData];
[toDoTableView deselectRow:rowToBeDeleted];
}
~~~~~~~~~~~
// This method should return the cell value of the selected row
- toDoTableCell:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
return toDoTableCell; // No errors here but toDoTableCell is nil.
}
The 'Add' data to tableView works, 'Delete' data from tableView works, I just cannot get the edited data out of tableView so I can reload the data with the corrections.
What you are looking for is an NSTableView Delegate method:
- (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
This will return the NSCell that is in the row and column that you specify. From the NSCell you should be able to extract the value that you need. Depending on how you are using your NSCell you would either call [cell stringValue] or [cell objectValue].
Try this:
– tableView:setObjectValue:forTableColumn:row:
in - NSTableViewDataSource Protocol Reference
--- Edited based on comment ---
Above method is called whenever user tries to edit a table row, it also provides user with changed value as parameter. If you are trying to edit the row in table itself then it should serve your purpose. You can simply check the objectValue obtained as parameter and verify if it is correct or not. In case it is incorrect you can modify the obtained value and set it in todoArray.
Briefly:
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
// below case is an example, you can add your own
if([anObject isEqualToString:#"Incorrect"])
{
anObject = #"Correct";
}
// Considering todoArray is array of dictionary items containing keys as table-column identifiers
NSMutableDictionary *originalData = [todoArray objectAtIndex:rowIndex];
[originalData setValue:anObject forKey:[aTableColumn identifier]];
[toDoTableView reloadData];
}
To get the value being edited you can simply use this code in above method, before setting the new value:
NSString *editedValue = [[todoArray objectAtIndex:rowIndex] valueForKey:[aTableColumn identifier]];
Hope this helps :)
It is simple. Read up on Cocoa Bindings and NSArrayController.
Take a look at the NSTableView methods selectedColumn, selectedColumnIndexes, selectedRow and selectedRowIndexes. I guess they should provide you with the needed information.
Now you can query the model, i.e. the array, for the data you need.
I have around 1000+ core data objects for my application. The application consist of a mapView and a tableView.
I am interested in displaying only the annotations that are appearing in the current mapRect. Hence I am using the method
NSSet *nearbySet = [self.mapView annotationsInMapRect:self.mapView.visibleMapRect];
The above method will return me a set of annotations available in the current mapView.
Now I only want to display the information based on the annotations returned from the above method in the table view (CoreData Table). How can I query core data for this? Can I use the predicate method with the NSFetchResultController? Any advice on this will be greatly appreicated!
Zhen Hoe
EDIT (10.05.2011):
My location data model is as such:
#dynamic uniqueID;
#dynamic name;
#dynamic address;
#dynamic longitude;
#dynamic latitude;
I have implemented the CoreDataTableViewController as shown below:
- (NSFetchedResultsController *)fetchedResultsControllerForTableView:(UITableView *)tableView
{
if (tableView == self.tableView)
{
if (self.fetchedResultsController.fetchRequest.predicate != normalPredicate)
{
[NSFetchedResultsController deleteCacheWithName:self.fetchedResultsController.cacheName];
self.fetchedResultsController.fetchRequest.predicate = normalPredicate;
[self performFetchForTableView:tableView];
}
[currentSearchText release];
currentSearchText = nil;
}
}
I was thinking of modifying the line 'self.fetchedResultsController.fetchRequest.predicate = normalPredicate" to restrict the fetch to only the objects found in my nearbySet. Is this a good place to do it? Also if you can give me some tips on how to write the predicate part, that will be great!
Thanks
Zhen Hoe