rearranging table rows by dragging in Lion - objective-c

I am having trouble using the new Lion functionality to rearrange rows in my app. I am using outlineView:pasteboardWriterForItem: to store the row indexes so that I can access them later when I validate/accept the drop. I create a new NSPasteboardItem to return, and am attempting to store the row number as so:
[pbItem setData: [NSKeyedArchiver archivedDataWithRootObject: [NSNumber numberWithInteger: [fTableView rowForItem: item]]]
forType: TABLE_VIEW_DATA_TYPE];
TABLE_VIEW_DATA_TYPE is a custom string I'm using to distinguish my custom data in the dragging pasteboard. I don't use it outside of dragging these rows.
When attempting the drag, I receive in Console: 'TableViewDataType' is not a valid UTI string. Cannot set data for an invalid UTI.
Of course I could use some of the built-in UTIs for pasteboards, but none of them apply (and using them causes the drag to accept drags other than the rows, which it shouldn't). Is there something I'm missing, like a way to define a custom UTI just for dragging (without making it a "real" UTI since I have no use for it outside of the internal dragging, so it shouldn't be public).
Thanks for any help!

I had similar requirements except I had a grid of objects that I wanted to rearrange by dragging selected objects to a new location. There are several ways of doing this, including creating a custom object and implementing the NSPasteboardWriting and NSPasteboardReading protocols, (and NSCoding protocols if you will be reading data as NSPasteboardReadingAsKeyedArchive), but this seems to be overkill for dragging of objects that remain internal to the application.
What I did involves using the NSPasteboardItem as a wrapper with a custom UTI type (it already implements the NSPasteboardWriting and NSPasteboardReading protocols). First declare a custom UTI type:
#define kUTIMyCustomType #“com.mycompany.MyApp.MyCustomType”
This needs to be defined in the ‘com.domain.MyApp’ format otherwise you will get errors of the form: “XXX is not a valid UTI string. Cannot set data for an invalid UTI.” Apple mentions this in their documentation.
Then you must register this custom UTI type in the view in which your dragging will occur. This can be done at runtime, and does not require any .plist additions. In your view's init method add the following:
[self registerForDraggedTypes:[NSArray arrayWithObjects:(NSString *)kUTIMyCustomType, nil]];
Now, make sure that the delegate is set for this view, and the delegate object implements the required NSDraggingSource and NSDraggingDestination protocol methods. This will allow you to avoid breaking the MVC design pattern, by allowing the designated controller object to handle placing the data on the pasteboard which will likely involve querying model data (i.e., indexes).
Specifically, for placing on the dragging pasteboard the indexes of objects to be moved when dragging begins as NSPasteboardItem wrappers of your index data:
- (void) draggingSession:(NSDraggingSession *)session willBeginAtPoint:(NSPoint)screenPoint
{
NSPasteboard * pboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pboard clearContents];
NSMutableArray * selectedIndexes = [NSMutableArray array];
// Add NSString indexes for dragged items to pasteboard in NSPasteboardItem wrappers.
for (MyModel * myModel in [self selectedObjects])
{
NSPasteboardItem * pasteboardItem = [[[NSPasteboardItem alloc] init] autorelease];
[pasteboardItem setString:[NSString stringWithFormat:#"%#", [myModel index]]
forType:kUTIMyCustomType];
[selectedIndexes addObject:pasteboardItem];
}
[pboard writeObjects:selectedIndexes];
}
And when the dragging operation completes, to read the dragged index NSPasteboardItem data:
- (BOOL) performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard * pasteboard = [sender draggingPasteboard];
// Check for custom NSPasteboardItem's which wrap our custom object indexes.
NSArray * classArray = [NSArray arrayWithObject:[NSPasteboardItem class]];
NSArray * items = [pasteboard readObjectsForClasses:classArray options:[NSDictionary dictionary]];
if (items == nil)
return NO;
// Convert array of NSPasteboardItem's with NSString index reps. to array of NSNumber indexes.
NSMutableArray * indexes = [NSMutableArray array];
for (NSPasteboardItem * item in items)
[indexes addObject:[NSNumber numberWithInteger:[[item stringForType:kUTIMyCustomType] integerValue]]];
//
// Handle dragged indexes…
//
return YES;
}

Another technique you can use is to just store the indices of the objects you're dragging in an instance variable on the side. Putting everything on the pasteboard isn't strictly necessary unless you're accepting items from another app or vice versa.
In awakeFromNib, register for the NSStringPboardType.
In …pasteboardWriterForRow, return [NSString string].
In …draggingSession:willBegin…, set your instance variable to the indices you want to track.
In validateDrop, return NSDragOperationNone if your instance variable is nil or the view is not yours.
In …draggingSession:ended…, nil out your instance variable.
Hope that helps… I'm using the technique for a table view, but it should be virtually identical for an outline view.

Instead of using a vanilla NSPasteboardItem, you should create a custom object that conforms to the NSPasteboardWriting protocol.
In your custom object, you can implement writableTypesForPasteboard: to return a list of custom UTIs that your pasteboard item supports. You then implement pasteboardPropertyListForType: to return a plist representation of your object for the appropriate custom UTI when the pasteboard asks for it.
You can create a plist from arbitrary data using the +propertyListWithData:options:format:error: method of NSPropertyListSerialization.
You would then override tableView:pasteboardWriterForRow: in your table view data source to return an instance of your custom object.

Related

Adding managedObjectContext objects with an NSArrayController

I have this application that is using core data and an NSArrayController to manage some objects in a table. I have the code below to pick up some objects on a directory. My questions is about the section below labeled "Handle Files". I create a new Video object using the url, I copy the metadata attributes using a custom function I wrote. The object is now inserted in the managedObjectContext. My question is, since I have my NSArrayController bound to my managedObjectContext, why do I have to still do [self addObject:newVideo] to have the object shown on my table? Is there a way to force the array controller to pull the object from the managedObjectContext without having to manually add it? It will be a hassle having to be updating both things every time I add or remove an object.
for (NSURL *url in _dirEnumerator) {
NSNumber *_isDirectory = nil;
[url getResourceValue:&_isDirectory forKey:NSURLIsDirectoryKey error:NULL];
if (![_isDirectory boolValue]) {
if (([_mediaTypes containsObject:[[url pathExtension]uppercaseString]])) {
// Handle the files
Video *newVideo = [NSEntityDescription insertNewObjectForEntityForName:#"Video" inManagedObjectContext:_managedObjectContext];
[newVideo copyAttributesFrom:url];
[self addObject:newVideo];
NSLog(#"Inserting video: %#",[newVideo valueForKey:#"name"]);
}
}
}
Well, I had my bindings all wrong an the array controller was not feeding my table correctly. You cannot sneak objects behind the array controller, if you implement the array controller you must let him do his job and that includes adding and removing objects. He will take care of letting the tableview know when things have changed.

How to Return a Custom Objective-C Object from a Class Method?

I have a set of Objective-C files in my app labeled Crop.h/Crop.m. Inside is a custom array of different vegetables from a video game - each vegetable has properties like a name, growing season, number of days to grow, etc.
I need to write a function which loops through this array and checks each Crop name value to see if it matches with my view title (self.title). Below is an example of two crop objects:
Crop *turnip = [[Crop alloc] init];
[turnip setName:#"Turnip"];
[turnip setSeason:#"Spring"];
[turnip setBagPrice:120];
[turnip setSaleValue:60];
[turnip setDaysToGrow:5];
[turnip setRenewableDays:0];
Crop *potato = [[Crop alloc] init];
[potato setName:#"Potato"];
[potato setSeason:#"Spring"];
[potato setBagPrice:150];
[potato setSaleValue:80];
[potato setDaysToGrow:8];
[potato setRenewableDays:0];
Then what I'm thinking is calling a new class function titled returnCropData: which takes the self.title as a parameter. I'm just not sure if this is my best method... I'd appreciate any suggestions from iOS devs.
Here's the simple class method I have so far - no data matching yet as I'm still trying to figure out which loop would be best. I'm also struggling to figure out the syntax to add an NSString parameter onto the function
+ (Crop *)returnCropData
{
Crop *turnip = [[Crop alloc] init];
[turnip setName:#"Turnip"];
[turnip setSeason:#"Spring"];
[turnip setBagPrice:120];
[turnip setSaleValue:60];
[turnip setDaysToGrow:5];
[turnip setRenewableDays:0];
// more crops here....
NSMutableArray *cropListing = [[NSMutableArray alloc] init];
[cropListing addObject:turnip];
[cropListing addObject:potato];
// add the other crops here...
return potato;
// return any Crop value for now
}
Apple's Objective-C docs recommend you use objectWithParams: grammar for class methods (constructors). So in your case, you should change your existing method name to
+ (Crop *)cropWithName:(NSString *)name
And call it with Crop *turnip = [Crop cropWithName:#"Turnip];
and then in your for loop you can check if the passed in name equals the name of your object with [name isEqualToString:turnip.name].
This will work, however it seems as though every time that method is called you're creating a ton of Crop objects - time intensive on your part, and memory intensive on the device. Instead, you should look into making a plist file that uses a dictionary to represent each kind of Crop, and then in your creation method you can use your passed in name to look up all the other information about the specified crop. Then you can just return one instance of Crop instead of instantiating a massive amount.
Here's a link to Apple documentation that explains plists:
https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/PropertyLists/Introduction/Introduction.html
Specifically, in any objects that I want to pull from a plist I define the following method that takes values from a dictionary and sets an object's properties:
- (id)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
[self setValuesForKeysWithDictionary:dictionary];
return self;
}
And then in a class where I want to access an array of data, I would create a synthesized #property and then define this method (in this case, I've got Song objects):
- (NSMutableArray *)songs
{
if (_songs == nil)
{
NSArray *songDicts = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ChillMix" ofType:#"plist"]];
_songs = [[NSMutableArray alloc] initWithCapacity:[songDicts count]];
// Fast enumeration //
for (NSDictionary *currDict in songDicts)
{
Song *song = [[Song alloc] initWithDictionary:currDict];
[_songs addObject:song];
}
}
return _songs;
}
Here is a sample project I've developed: https://github.com/Jlawr3nc3/MusicLibraryiOS
[NSArray arrayWithContentsOfFile:] should be your new best friend. It's awesome. If you can format your XML in plist format, you're down to one. single. line. to parse data. (PS normal XML is annoying as hell to parse on iOS so this is a huge time save if you can avoid straight up XML).
Also, I'm not sure what the use case would be for creating a crop object based on a view's title. Since you're already aware of the name of the crop before the view is instantiated, you could create a Crop object and set a Crop property of the view to the crop object you created, and then in viewDidLoad just do self.title = self.crop.name;. Finally, if you've got a UITableView of crop objects you shouldn't populate the table view with static text; instead populate it with objects you create from a plist, and then in didSelectRowAtIndexPath: you can do [self.arrayOfCrops objectAtIndex:indexPath.row] to get the object you tapped on, and pass it in with the view you load. Anyway, check out Apple's sample code for more info on plists and tableviews etc, 'cause they've got a lot of information on this exact stuff.

What's the way to communicate a set of Core Data objects stored in the background to the main thread?

Part of my iOS project polls a server for sets of objects, then converts and saves them to Core Data, to then update the UI with the results. The server tasks happens in a collection of NSOperation classes I call 'services' that operate in the background. If NSManagedObject and its ~Context were thread safe, I would have had the services call delegate methods on the main thread like this one:
- (void)service:(NSOperation *)service retrievedObjects:(NSArray *)objects;
Of course you can't pass around NSManagedObjects like this, so this delegate method is doomed. As far as I can see there are two solutions to get to the objects from the main thread. But I like neither of them, so I was hoping the great StackOverflow community could help me come up with a third.
I could perform an NSFetchRequest on the main thread to pull in the newly added or modified objects. The problem is that the Core Data store contains many more of these objects, so I have to add quite some verbosity to communicate the right set of objects. One way would be to add a property to the object like batchID, which I could then pass back to the delegate so it would know what to fetch. But adding data to the store to fix my concurrency limitations feels wrong.
I could also collect the newly added objects' objectID properties, put them in a list and send that list to the delegate method. The unfortunate thing though is that I have to populate the list after I save the context, which means I have to loop over the objects twice in the background before I have the correct list (first time is when parsing the server response). Then I still only have a list of objectIDs, which I have to individually reel in with existingObjectWithID:error: from the NSManagedObjectContext on the main thread. This just seems so cumbersome.
What piece of information am I missing? What's the third solution to bring a set of NSManagedObjects from a background thread to the main thread, without losing thread confinement?
epologee,
While you obviously have a solution you are happy with, let me suggest that you lose some valuable information, whether items are updated, deleted or inserted, with your mechanism. In my code, I just migrate the userInfo dictionary to the new MOC. Here is a general purpose routine to do so:
// Migrate a userInfo dictionary as defined by NSManagedObjectContextDidSaveNotification
// to the receiver context.
- (NSDictionary *) migrateUserInfo: (NSDictionary *) userInfo {
NSMutableDictionary *ui = [NSMutableDictionary dictionaryWithCapacity: userInfo.count];
NSSet * sourceSet = nil;
NSMutableSet *migratedSet = nil;
for (NSString *key in [userInfo allKeys]) {
sourceSet = [userInfo valueForKey: key];
migratedSet = [NSMutableSet setWithCapacity: sourceSet.count];
for (NSManagedObject *mo in sourceSet) {
[migratedSet addObject: [self.moc objectWithID: mo.objectID]];
}
[ui setValue: migratedSet forKey: key];
}
return ui;
} // -migrateUserInfo:
The above routine assumes it is a method of a class which has an #property NSManagedObjectContext *moc.
I hope you find the above useful.
Andrew
There's a section of the Core Data Programming Guide that addresses Concurrency with Core Data. In a nutshell, each thread should have its own managed object context and then use notifications to synchronize the contexts.
After a little experimentation, I decided to go for a slight alteration to my proposed method number 2. While performing background changes on the context, keep a score of the objects you want to delegate back to the main thread, say in an NSMutableArray *objectsOfInterest. We eventually want to get to the objectID keys of all the objects in this array, but because the objectID value changes when you save a context, we first have to perform that [context save:&error]. Right after the save, use the arrayFromObjectsAtKey: method from the NSArray category below to generate a list of objectID instances, like so:
NSArray *objectIDs = [objectsOfInterest arrayFromObjectsAtKey:#"objectID"];
That array you can pass back safely to the main thread via the delegate (do make sure your main thread context is updated with mergeChangesFromContextDidSaveNotification by listening to the NSManagedObjectContextDidSaveNotification). When you're ready to reel in the objects of the background operation, use the existingObjectsWithIDs:error: method from the category below to turn the array of objectID's back into a list of working NSManagedObjects.
Any suggestions to improve the conciseness or performance of these methods is appreciated.
#implementation NSArray (Concurrency)
- (NSArray *)arrayFromObjectsAtKey:(NSString *)key {
NSMutableArray *objectsAtKey = [NSMutableArray array];
for (id value in self) {
[objectsAtKey addObject:[value valueForKey:key]];
}
return objectsAtKey;
}
#end
#implementation NSManagedObjectContext (Concurrency)
- (NSArray *)existingObjectsWithIDs:(NSArray *)objectIDs error:(NSError **)error {
NSMutableArray *entities = [NSMutableArray array];
#try {
for (NSManagedObjectID *objectID in objectIDs) {
// existingObjectWithID might return nil if it can't find the objectID, but if you're not prepared for this,
// don't use this method but write your own.
[entities addObject:[self existingObjectWithID:objectID error:error]];
}
}
#catch (NSException *exception) {
return nil;
}
return entities;
}
#end

Passing array of custom objects to NSCell in NSMatrix programmatically

I have an NSArray of custom NSObjects. Each object has some properties and an image that I would like to display in a grid view. NSMatrix appears to be a good solution to my problem, but I am having issues getting the content of the objects to display.
Couple of things to note.
I am not using core data
I am trying to do this programmatically
I have considered using NSCollectionView but NSMatrix appears to be a better solution in this case
All the cells follow the same display format as each other - i.e. I'm not wanting to pass different cells different types of objects, just a different instance of the object
Assume I have an NSView (matrixContainerView) in a window. The controller file has an IBOutlet to matrixContainerView. In my controller I have the following in my awakeFromNib:
NSMatrix* matrix = [[NSMatrix alloc]
initWithFrame:[matrixContainerView bounds]
mode:NSRadioModeMatrix
cellClass:[MyCustomCell class]
numberOfRows:5
numberOfColumns:5];
[matrix setCellSize:NSMakeSize(116, 96)];
[matrix setNeedsDisplay:YES];
[matrixContainerView addSubview:[matrix autorelease]];
[matrixContainerView setNeedsDisplay:YES];
The class MyCustomCell header looks like the following:
#interface MyCustomCell : NSCell {
MyModel * theObject;
}
-(MyModel *)theObject;
-(void)setTheObject:(MyModel *)newValue;
And the implementation file as follows (drawing simplified):
#implementation MyCustomCell
-(void)drawInteriorWithFrame:(NSRect)theFrame inView:(NSView *)theView {
...drawing code using MyModel e.g. [MyModel isValid] etc...
}
-(MyModel *)theObject {
return theObject;
}
-(void)setTheObject:(MyModel *)newValue {
[theObject autorelease];
theObject = [newValue retain];
}
#end
After some initialization and population of the array containing MyModel objects in the controller, I want to populate the NSMatrix with instances of the objects.
How do I do this?
I have tried adding just two objects from the array as follows (just as a test):
MyCustomCell * cellOne = (MyCustomCell *)[matrix cellAtRow:0 column:0];
[cell setTheObject:[myArrayOfObjects objectAtIndex:0]];
MyCustomCell * cellTwo = (MyCustomCell *)[matrix cellAtRow:0 column:1];
[cellTwo setTheObject:[myArrayOfObjects objectAtIndex:1]];
But this just creates the first object image. If the above had worked, it would been a straightforward task of enumerating through the array and adding the objects.
How do I go about adding the cells and passing the appropriate objects to those cells in order that they can be displayed correctly?
The Apple docs are sparse to say the least on NSMatrix as far as the programming guide goes. The information in there is very useful to me, but only after I have added the objects and got them displaying!
Update
If I do not add the two objects (as per my example above) the output is no different, i.e. a single representation of my custom cell is drawn to screen. This tells me that the single representation I see is being done at the initialization of the matrix and in fact I wasn't drawing anything to column 0 row 0 when in fact I thought I was. Which leaves me now more confused.
Might be that the matrix actually has the two cells but its frame is too small to display them?
After adding the cells try calling [matrix sizeToCells]

Issues declaring already existing NSMutableArray in new class

I have a class (DataImporter) which has the code to download an RSS feed. I also have a view and separate class (TableView) which displays the data in a UITableView and starts the parsing process, storing parsed information in an NSMutableArray (items) which is located in the (TableView) subclass.
Now I wish to add a UIMapView which displays the items in the (items) NSMutableArray. Herein lies the issue - I need to somehow get the data from the (items) NSMutableArray into the new (mapView) subclass which I'm struggling with - and I preferably don't want to have to create a new class to download the data again for the mapView class when it already is in the applications memory. Is there a way I can transfer the information from the NSMutableArray (items) class to the (mapView) class (i.e. how do I declare the NSMutableArray in the (mapView) class)?
Here's a overview of how the system works:
App opened> Data downloaded (using DataImporter class) when (TableView) viewDidLoad runs> Data stored in NSMutableArray accessible by the (TableView) class> And from here I need to access and declare the array from a new (mapView) class.
Any help greatly appreciated, thanks.
Code for viewDidLoad MapKit:
Data *data = nil;
NSString *ilocation = [data locations];
NSString *ilocation2 = #"New Zealand";
NSString *inewlString;
inewlString = [ilocation stringByAppendingString:ilocation2];
NSLog(#"inewlString=%#",inewlString);
if(forwardGeocoder == nil)
{
forwardGeocoder = [[BSForwardGeocoder alloc] initWithDelegate:self];
}
// Forward geocode!
[forwardGeocoder findLocation: inewlString];
Code for parsing data into original NSMutable Array:
- (void)beginParsing {
NSLog(#"Parsing has begun");
//self.navigationItem.rightBarButtonItem.enabled = NO;
// Allocate the array for song storage, or empty the results of previous parses
if (incidents == nil) {
NSLog(#"Grabbing array");
self.datas = [NSMutableArray array];
} else {
[datas removeAllObjects];
[self.tableView reloadData];
}
// Create the parser, set its delegate, and start it.
self.parser = [[DataImporter alloc] init];
parser.delegate = self;
[parser start];
}
Since you have a property self.datas you can get a reference to the data just by calling that from your UIMapView, or by assigning to some target property in your map view from whatever object controls both views:
mapView.someArrayProperty = tableView.datas
However, you might want to rethink your overall program structure. Keeping what is in effect model data inside views is not good practice and tends make things pretty spaghetti-like as you add more objects and functionality.