I want to provide custom sorting using NSFetchedResultsController and NSSortDescriptor.
As custom sorting via NSSortDescriptor message -(id)initWithKey:ascending:selector: is not possible (see here), I tried to use a NSSortDescriptor derived class in order to override the compareObject:toObject: message.
My problem is that the compareObject:toObject: is not always called. It seems that it is called only when the data are already in memory. There is an optimization of some sort that use a database based sort instead of the compareObject:toObject when the data are retrieved from the store the first time. (see here).
My question is : how to force NSFetchedResultscontroller to use the compareObject:toObject: message to sort the data ? (and will it work with large data set)
One solution is to use a binary store instead of a sqlite store but I don't want to do that.
Another solution is:
-call performFetch to sort data via SQL (compareObject not called)
-make a modification to the data and reverse it.
-call performFetch again (compareObject is called)
It does work in my case but it's a hack and I am not sure it will always work (especially with large data set (greater than the batch size)).
UPDATED:You can reproduce with the CoreDataBooks sample.
In RootViewController.m, add this ugly hack:
- (void)viewWillAppear:(BOOL)animated {
Book* book = (Book *)[NSEntityDescription insertNewObjectForEntityForName:#"Book"
inManagedObjectContext:[self fetchedResultsController].managedObjectContext];
[[self fetchedResultsController] performFetch:nil];
[[self fetchedResultsController].managedObjectContext deleteObject:book];
[self.tableView reloadData];
}
In RootViewController.m, replace the sort descriptor code with:
MySortDescriptor *myDescriptor = [[MySortDescriptor alloc] init];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:myDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
Add MySortDescriptor class:
#implementation MySortDescriptor
-(id)init
{
if (self = [super initWithKey:#"title" ascending:YES selector:#selector(compare:)])
{
}
return self;
}
- (NSComparisonResult)compareObject:(id)object1 toObject:(id)object2
{
//set a breakpoint here
return [[object1 valueForKey:#"author" ] localizedCaseInsensitiveCompare:[object2 valueForKey:#"author" ] ];
}
//various overrides inspired by [this blog post][3]
- (id)copy
{
return [self copyWithZone:nil ];
}
- (id)mutableCopy
{
return [self copyWithZone:nil ];
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
return [self copyWithZone:zone ];
}
- (id)copyWithZone:(NSZone*)zone
{
return [[MySortDescriptor alloc] initWithKey:[self key] ascending:[self ascending] selector:[self selector]];
}
- (id)reversedSortDescriptor
{
return [[[MySortDescriptor alloc] initWithKey:[self key] ascending:![self ascending] selector:[self selector]] autorelease];
}
#end
In reference to your question and the comments. You are going to need to pull the objects into memory to sort them. Once they are in memory you can use a convenience method to determine distance from a point.
To decrease the number of objects you pull into memory you could calculate max and min values and then filter on those, reducing the radius of your search before you sort.
It is not possible to sort on a calculated value unless it is in memory.
Related
I am putting the finishing touches on an App, and have difficulties with removing records in bulk. On the hit of a button a set of approx. 3500 records need to be added to the database. This is not a problem, and takes approx. 3-4 seconds.
But sometimes (not often, but the option needs to be there) all these records need to be removed. I just ran this operation, and it took 20 minutes. What could be wrong here? There is only one dependency, all the records are children of a particular Collection.
I add all the items to a set, removed them from the Collection and then delete one by one. Every 5% I update the dialogue and when everything is done I commit the changes. But removing the items just takes ages (as I can see the progress dialogue progress very slowly)
- (void) deleteList:(DOCollection *) collection {
// For the progress dialogue
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Clearing vocabulary list!" forKey:#"message"];
float totalItems = [collection.items count];
float progress = 0;
float nextProgressRefresh = 0.05;
NSMutableSet* itemsSet = [NSMutableSet set];
for (DOItem* item in collection.items) {
[itemsSet addObject:(NSNumber*)[NSNumber numberWithInt:[item.itemId intValue]]];
}
// Remove all of them from the collection
[managedObjectContext performBlockAndWait:^{
[collection setItems:[NSSet set]];
}];
for (NSNumber* itemId in itemsSet) {
DOItem* item = [itemController findItem:[itemId intValue]];
if (item != nil) {
[[self itemController] removeItem:item];
}
progress++;
if((nextProgressRefresh < (progress / totalItems))){
NSString* sProgress = [NSString stringWithFormat:#"%f", (progress / totalItems) * 0.85];
//[dict setValue:#"Saving the database...!" forKey:#"message"];
[dict setValue:sProgress forKey:#"progress"];
[[NSNotificationCenter defaultCenter] postNotificationName:kUpdatePleaseWaitDialogue object:dict];
nextProgressRefresh = nextProgressRefresh + 0.05;
}
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[managedObjectContext performBlockAndWait:^{
[[self collectionController] commitChanges];
}];
[[NSNotificationCenter defaultCenter] postNotificationName:kSavingDataComplete object:nil];
});
//NSLog(#"Wait 2!");
[NSThread sleepForTimeInterval:1];
}
in DOItemController:
- (void) removeItem: (NSManagedObject*) item {
[[self managedObjectContext] deleteObject:item];
}
Not sure about how you architected your data models. But I would set it up to cascade delete your objects. If the DOItem Objects are unique to the DOCollection, then you can set the delete rule to cascade. That will automagically delete the associated DOItem as well as remove it from the DOCollection item set object.
To delete the DOItem objects from the DOCollection, check your DOCollection.h file, you should have a method along the lines of
-(void)removeDOItemObjects:(NSSet *)value
if not, they may still be dynamically generated by Core Data for you. In your header file you should have something along the lines of:
#property(nonatomic,retain) DOItem *items
then in your implementation file, you should have something along the lines of:
#synthesize items
The appropriate methods that should be generated for these automatically:
-(void)addItemsObject:(DOItem*)value
-(void)addItems:(NSSet *)values
-(void)removeItemsObject:(DOItem *)value
-(void)removeItems:(DOItem *)values
-(NSSet *)items
See "Custom To-Many Relationship Accessor Methods" here for more info.
This method is provided for you when you create the data model and associated implementation files and should be highly optimized by Core Data. Then all you have to do to remove the objects is something along the lines of:
- (void) deleteList:(DOCollection *) collection {
// Remove all of them from the collection
[managedObjectContext performBlockAndWait:^{
// Updated 01/10/2012
[collection removeItems:collection.items];
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Core Data: Error saving context."); }
};
}];
}
You might want to check the performance of the delete using this method and continue to provide the user with feedback. If performance is an issue, consider striding, where you divide the set up into chunks and perform the above method fore each one of the strides, update the user interface, etc.
Again, am not sure about your application architecture, but on first glance, this looks like the issue.
Good Luck!
I get the error
* Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSCFSet: 0x6b66390> was mutated while being enumerated.'
when adding an new delegate to my class. Or at least, that's where I think the problem is.
This is my code: MyAppAPI.m
[...]
static NSMutableSet *_delegates = nil;
#implementation MyAppAPI
+ (void)initialize
{
if (self == [MyAppAPI class]) {
_delegates = [[NSMutableSet alloc] init];
}
}
+ (void)addDelegate:(id)delegate
{
[_delegates addObject:delegate];
}
+ (void)removeDelegate:(id)delegate
{
[_delegates removeObject:delegate];
}
[...]
#end
MyAppAPI is a singleton which I can use throughout my application. Wherever I can (or should be able to) do: [MyAppAPI addDelegate:self].
This works great, but only in the first view. This view has a UIScrollView with PageViewController which loads new views within itself. These new views register to MyAppAPI to listen to messages until they are unloaded (which in that case they do a removeDelegate).
However, it seems to me that it dies directly after I did a addDelegate on the second view in the UIScrollView.
How could I improve the code so that this doesn't happen?
Update
I'd like to clarify me a bit further.
What happens is that view controller "StartPage" has an UIScrollView with a page controller. It loads several other views (1 ahead of the current visible screen).
Each view is an instans PageViewController, which registers itself using the addDelegate function shown above to the global singleton called MyAppAPI.
However, as I understand this viewcontroller 1 is still reading from the delegate when viewcontroller 2 registers itself, hence the error shows above.
I hope I made the scenario clear. I have tried a few things but nothing helps.
I need to register to the delegate using addDelegate even while reading from the delegates. How do I do that?
Update 2
This is one of the reponder methods:
+ (void)didRecieveFeaturedItems:(NSArray*)items
{
for (id delegate in _delegates)
{
if ([delegate respondsToSelector:#selector(didRecieveFeaturedItems:)])
[delegate didRecieveFeaturedItems:items];
}
}
Scott Hunter is right. This error is thrown when you try to edit a list while iterating.
So here is an example of what you may be doing.
+ (void)iteratingToRemove:(NSArray*)items {
for (id delegate in _delegates) {
if(delegate.removeMePlease) {
[MyAppAPI removeDelegate:delegate]; //error you are editing an NSSet while enumerating
}
}
}
And here is how you should handle this correctly:
+ (void)iteratingToRemove:(NSArray*)items
{
NSMutableArray *delegatesToRemove = [[NSMutableArray alloc] init];
for (id delegate in _delegates) {
if(delegate.removeMePlease) {
[delegatesToRemove addObject:delegate];
}
}
for(id delegate in delegatesToRemove) {
[MyAppAPI removeDelegate:delegate]; //This works better
}
[delegatesToRemove release];
}
The error suggests that, while some code somewhere is in the middle of going through your list, you are modifying the list (which explains the crash after addDelegate is called). If the code doing the enumerating is the one modifying the list, then you just have to put off the modifications until the enumeration is done (say, by collecting them up in a different list). Without knowing anything about the code doing the enumerating, can't say much more than that.
A simple solution, don't use a mutable set. They are dangerous for a variety of reasons, including this one.
You can use -copy and -mutableCopy to convert between mutable and non-mutable versions of NSSet (and many other classes). Beware all copy methods return a new object with a retain count of 1 (just like alloc), so you need to release them.
Aside from having less potential for bugs, non-mutable objects are faster to work with and use less memory.
[...]
static NSSet *_delegates = nil;
#implementation MyAppAPI
+ (void)initialize
{
if (self == [MyAppAPI class]) {
_delegates = [[NSSet alloc] init];
}
}
+ (void)addDelegate:(id)delegate
{
NSMutableSet *delegatesMutable = [_delegates mutableCopy];
[delegatesMutable addObject:delegate];
[_delegates autorelease];
_delegates = [delegatesMutable copy];
[delegatesMutable release];
}
+ (void)removeDelegate:(id)delegate
{
NSMutableSet *delegatesMutable = [_delegates mutableCopy];
[delegatesMutable removeObject:delegate];
[_delegates autorelease];
_delegates = [delegatesMutable copy];
[delegatesMutable release];
}
[...]
#end
Scott Hunter is right - it's a problem with modifying the NSSet while you're enumerating over the set's items. You should have a stack trace from where the application crashes. It probably has a line where you're adding to/remove from the _delegates set. This is where you need to make the modification. It's easy to do. Instead of adding to/deleting from the set, do the following:
NSMutableSet *tempSet = [_delegates copy];
for (id delegate in _delegates)
{
//add or remove from tempSet instead
}
[_delegates release], _delegates = tempSet;
Additionally, NSMutableSet is not thread safe, so you should call your methods always from the main thread. If you haven't explicitly added any extra threads, you have nothing to worry about.
A thing to always remember about the Objective-C "fast enumeration".
There is 2 big difference between "fast enumeration" and a for loop.
"fast enumeration" is quicker than a for loop.
BUT
You can't modify the collection your enumerating over.
You can ask your NSSet for - (NSArray *)allObjects and enumerate over that array while modifying your NSSet.
You get this error when a thread tries to modify (add,delete) the array while other thread is iterating over it.
One way to solve this using NSLock or synchronizing the methods. That ways add, remove and iterate methods cannot be called in parallel.
But this will have effect on performance and/or responsiveness because any add/delete will have to wait for the thread that was iterating over the array.
A better solution inspired from Java's CopyOnWriteArrayList would be to create a copy of the array and iterate over the copy. So the only change in your code will be:-
//better solution
+ (void)didRecieveFeaturedItems:(NSArray*)items
{
NSArray *copyOfDelegates = [_delegates copy]
for (id delegate in copyOfDelegates)
{
if ([delegate respondsToSelector:#selector(didRecieveFeaturedItems:)])
[delegate didRecieveFeaturedItems:items];
}
}
Solution using locks with performance impact
//not a good solution
+ (void)addDelegate:(id)delegate
{
#synchronized(self){
[_delegates addObject:delegate];
}
}
+ (void)removeDelegate:(id)delegate
{
#synchronized(self){
[_delegates removeObject:delegate];
}
}
+ (void)didRecieveFeaturedItems:(NSArray*)items
{
#synchronized(self){
for (id delegate in _delegates)
{
if ([delegate respondsToSelector:#selector(didRecieveFeaturedItems:)])
[delegate didRecieveFeaturedItems:items];
}
}
}
hi :) I have a similarly issue like in Working with the same NSManagedObjectContext in multiple tabs
background:
My managedObjectContext (further MOC) is initialised in my appDelegate class and passed throught to multiple tabs by
myViewController.managedObjectContext = self.managedObjectContext; or in the init method with self.managedObjectContext = pContext;
the flow is: the first view is a simple list of collections. The collections are fetched with a NSFetchedResultsController (myViewController : UITableViewController<NSFetchedResultsControllerDelegate>). By selecting one, you navigate deeper, but still passing this MOC.
In the next controller (detailsViewController) I list up some items of this collection what I can interact with (set switches for instance).
I also have an editingObjectContext:
// DetailsViewController.m
NSManagedObjectContext* editingContext = [[NSManagedObjectContext alloc] init];
[editingContext setPersistentStoreCoordinator:[managedObjectContext persistentStoreCoordinator]];
self.editingObjectContext = editingContext;
Now my issue: because my view has to rotate, I am using the folowing trick:
// DetailsViewController.m
DetailsView *localAct = [[DetailsView alloc] initWithManagedObjectContext:managedObjectContext ... ]
DetailsView *localSen = [[DetailsView alloc] initWithManagedObjectContext:managedObjectContext ... ]
UITableView *localContainerView = [[UITableView alloc] init];
self.containerView = localContainerView;
[localContainerView release];
//[...]
[containerView addSubview:actuatorView];
self.tableView = containerView;
further I have a button to manage this items (which of them shall be shown and which not). This button just reloads the table with a new fetchResult.
// DetailsView.m
- (void) manageItems{
managing = !managing;
[viewController setIsManaging:managing]; // parent
self.fetchedResultsController = nil;
NSError *error = nil;
[[self fetchedResultsController] performFetch:&error];
[self reloadData];
[self updateBarButton];
}
The method for putting the items into the context looks so:
// DetailsViewController.m
(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// init + create predicate
NSSet* set = [sen filteredSetUsingPredicate:predicate];
if( [set count] > 0 )
{
for( Act* act in set )
{
[editingObjectContext deleteObject:act];
}
}
else
{
Act* act = [NSEntityDescription insertNewObjectForEntityForName:#"Act" inManagedObjectContext:editingObjectContext];
// do things
}
NSError *error = nil;
[[detailView fetchedResultsController] performFetch:&error];
[self.containerView reloadData];
[detailView reloadData];
}
but after I selected the items in the managed view and clicked save (manageItems), the view doesn't show them :/ i have to switch the tab or to navigate in an other controller (parent or deeper) to actualize it.
my ViewWillAppear method:
// DetailsViewController.m
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
DetailsView *detailView = se ? senView : actView;
// [do uninteresting stuff]
[detailView.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
// [do uninteresting stuff]
}
and viewWillDisapper calls
- (void)saveChanges
{
if( ![editingObjectContext hasChanges] )
return;
// send save-command to server
}
In an earliert Verison where there was only 1 view it worked and I haven't changed realy much... :/ so I don't understand why the MOC is acting like it does. The "manageItems" part is nearly equal, its just a level deeper in the new version (in the DetailsView instead of the controller) ...
if someone can tell me what I can try (always saving to server when switch between managing and normal isn't a solution because the delay in the response from the server is to high for the refresh, so I have the less to flip the view. Also refreshing the views with self.tableView / detailView / self.containerView refresh brings the same result :/ ).
and a second issue: I can't call the "editingObjectContext save:" method after sending to server, because it's throwing errors and don't save at all to local database.
Error in handleChangeResponse:
Error Domain=NSCocoaErrorDomain Code=133020 "The operation couldn’t be completed. (Cocoa error 133020.)" UserInfo=0x4d8bb90 {conflictList=(
"NSMergeConflict (0x5a2fac0) for NSManagedObject (0x5a46a80) with objectID '0x5a46420 ' with oldVersion = 7 and newVersion = 8 and old object snapshot = {\n iconName = noicon;\n [...] ;\n} and new cached row = {\n iconName = noicon;\n [...] \n}"
)}
if you have questions or need some more code (i.e. of the older version) then just ask ;)
thanks in anticipation :)
It seems like I have the solution! Since IOS 5.0 there is a new method for NSManagedObjectContext :
[managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
Found on http://pauloliveira.net/tech/core-data-merging-conflicts
Setting this attribute to the top-level MOC (in my case in the appDelegate) and no-where else! clears my merging problems ;)
I found the reason why it doesn't worked... forget everything what I wrote above... the problem was in the fetchrequest - concretely: in the predicate... in the earlier versions I used
[NSComparisonPredicate predicateWithLeftExpression: ...]
in the actualy version I use
NSString * predicateFormat = [NSString stringWithFormat: ...];
NSPredicate* predicate = [NSPredicate predicateWithFormat:predicateFormat];
because I had to extend the number of options and also edited the request itself because it made problems in the predicate (comparing a complete object (of the MOC class, extracted from the database) with an entity didn't worked, so I managed the workaround in the DetailsViewController and haven't rolled back my updates in this place :/).
Never thought to waste so much time on this problem >.< but okay, as long as it's resolved :D
I will check if the second issue (with the saving problem) still exists. If not, I will update my post, otherwise this topic isn't closed :/
This may be due to manageobject context in use of object where u'r getting this. Remove all NSManagebobject at the time when you either log out or move back. say end using app. Seems like this...
[NSManagebobjectcontext setManagedObjectsDictionary:[NSMutableDictionary dictionary]];
I have a UISegmentControl with my UITableView that sorts the data. I'd like to be able to do things:
(1) default sort (so when the user turns on the app for the first time, it would select the first segment, and sort by that action)
(2) remember where the user was between table loads. What I mean by this is, similar to Apple's coverflow, when I go to a different cover, the UITableView repopulates. So if the last time the user was there, the sort was on the 3rd segment, then it would remember that.
I'm a bit new to object-oriented design, and this was my best guess to not have the same redundant code everywhere: (MarkersList is a NSMutableArray)
- (NSArray *)sortByName:(NSArray *)sortDescriptors {
return [self.MarkersList sortedArrayUsingDescriptors:sortDescriptors];
}
- (NSArray *)sortByRSID:(NSArray *)sortDescriptors {
return [self.MarkersList sortedArrayUsingDescriptors:sortDescriptors];
}
- (void)setSortedMarkersList:(NSArray *)sortedArray {
if (self.MarkersList != nil) {
[self.MarkersList removeAllObjects];
}
[self.MarkersList addObjectsFromArray:sortedArray];
}
- (IBAction)sortButtonPressed:(UISegmentedControl *)segmentControl {
// Create sort descriptors
NSSortDescriptor *nameDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"Name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)] autorelease];
NSSortDescriptor *rsID = [[NSSortDescriptor alloc] initWithKey:#"ID" ascending:YES];
if ([segmentControl selectedSegmentIndex] == NAME) { // this is #define 0
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, rsIDDescriptor, nil];
NSArray *sortedArray = [self sortByGene:sortDescriptors];
[self setSortedMarkersList:sortedArray];
[sortDescriptors release];
}
else if ([segmentControl selectedSegmentIndex] == RS_ID) {
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:rsIDDescriptor, resultDescriptor, nameDescriptor, nil];
NSArray *sortedArray = [self sortByRSID:sortDescriptors];
[sortDescriptors release];
[self setSortedMarkersList:sortedArray];
}
[self.MarkersTableView reloadData];
}
I haven't implemented the third sort yet since it's not just an NSString or NSNumber like the other two yet. So far, I think it works correctly. However, the problem I have is to implement (1), I would need to call sortByName when my table is loaded. I could just create the NSSortDescriptors again, but that seems redundant. Is there a more OOD way to achieve this?
For (2), I'm guessing I could save that index for the table in a dictionary, and retrieve it when that table is loaded. Or something along those lines, not really sure.
Any help is greatly appreciated. Thanks!
This isn't so much an OO question, as iOS doesn't always let us use best practices to solve a problem. The best bet for your situation, I think, is to store the information on the selected sort in the NSUserDefaults. In your -viewDidLoad method, check if the selected sort object exists in NSUserDefaults, use it if it does, and if not choose a reasonable default value.
Don't worry about creating NSSortDescriptors with each load of the application, unless you have done profiling and determined that a large amount of time is spent building it. Serializing and deserializing the NSSortDescriptors would be far more inefficient than just recreating it when needed. Apple spends a lot of time optimizing frequently used classes like NSSortDescriptor.
Regarding #2, you can use indexPathsOfVisibleRows on the UITableView to get an array of visible indices, store the first one in UserDefaults, then on load (or pop from the next view controller if that occurs) call –scrollToRowAtIndexPath:atScrollPosition:animated:.
Once you're code is functioning, I recommend you watch the various videos on iTunes U regarding profiling and Instruments. It's a wonderful tool that is often overlooked, and really helps concentrate effort where it's needed.
can anyone clear this up for me ?
I am building an iPad App that has a TableViewController that is supposed to show something between 1000 and 2000 strings.
I have those NSStrings in a Singleton.
In the init Method of the Singleton I initialize an Array that holds all the data ( does not have to be the final way to do it - was just a quick copy and paste for testing )
I did an self.someArray = [[NSArray alloc]initWithObjects: followed by the large number of strings, followed by nil.
that worked fine in the simulator - but crashed with bad access on the iPad right on Application startup
If I use the convenience method [NSArray arrayWithObjects:instead - it works fine.
I looked into Instruments and the overall memory footprint of the App is just about 2,5 MB.
Now I don't know why it works the one way but not the other.
EDIT:
#import "StaticValueContainer.h"`
static StaticValueContainer* instance = nil;
#implementation StaticValueContainer
#synthesize customerRatingKeys;
+(StaticValueContainer*)sharedInstance
{
if (instance == nil){
instance = [[StaticValueContainer alloc]init];
}
return instance;
}
-(id)init
{
if ( ( self = [super init] ))
{
[self initCustomerRatingKeys];
}
return self;
}
-(void)init customerRatingKeys
{
self.customerRatingKeys = [[NSArray alloc]initWithObjects:
#"string1",
....
#"string1245"
,nil
}
as I said: it crashes on the device with self.customerRatingKeys = [[NSArray alloc]initWithObjects:
but works with *self.customerRatingKeys = [[NSArray arrayWithObjects...`
Well, there isn't much difference between them: arrayWithObjects returns an auto-released array that you don't need to release yourself (unless you subsequently retain it), and initWithObjects returns an array you must then release to avoid a memory leak. Performance wise there is no difference between them.
I would suggest if you're getting a bad access error using initWithObjects but not with arrayWithObjects there might be some sort of memory management error in your code. If you post the code itself you'll probably get a more exact response.