14

I am creating a tasks application which should support offline mode. I have used RestKit to download tasks and mapped that in local Core data.

This is working good in online mode. But in offline there is strange problem. I use NSPredicate to fetch data from local storage. For this I am using Magical Records.

+ (void)getIdeasTasksWithPageNo:(int)pageNo completionHandler:(void (^)(NSArray *, NSError *))completionHandler {

    NSArray *tasks = [self MR_findAllWithPredicate:[NSPredicate predicateWithFormat:@"due_date = nil AND user_id = %@", [DBUsers currentUser].id]];
    completionHandler(tasks, nil);
}

And I call it like this:

[DBTasks getIdeasTasksWithPageNo:1 completionHandler:^(NSArray *tasks, NSError *error) {
            if (!error) {
                [self displayTasksWithResults:tasks forPageNo:1];                   

            } else {
                NSLog(@"Error is %@", error);
            }
        }];

And this is how I am displaying it in UITableView

-(void)displayTasksWithResults:(NSArray *)tasks forPageNo:(int)pageNo {
    if (!self.tasksArray) {
        self.tasksArray = [[NSMutableArray alloc] init];

    } else {
        [self.tasksArray removeAllObjects];
    }
    [self.tasksArray addObjectsFromArray:tasks];
    [self.tableview reloadData];
}

This is working only for first time and all tasks are populated in UITableView.

Problem is after the UITableView is populated, all records in self.tasksArray become Null. If I scroll UITableView, the table rows start being empty.

But if I print self.tasksArray in displayTasksWithResults method, it prints perfectly.

(
    "Title: Task 01",
    "Title: You've gone incognito. Pages you view in incognito tabs won't stick around in your browser's history, cookie store, or search history after you've closed all of your incognito tabs. Any files you download or bookmarks you create will be kept. ",
    "Title: Task 06",
    "Title: Task 04",
    "Title: Hi",
    "Title: Task 3",
    "Title: Task 4",
    "Title: Hi 4",
    "Title: hh",
    "Title: Task 02",
    "Title: Task 05\n",
    "Title: Task 4",
    "Title: Task 5",
    "Title: Task 2 updated",
    "Title: Here is a task. ",
    "Title: Task 03",
    "Title: Hi 3",
    "Title: Task 2",
    "Title: Hi 2",
    "Title: Testing task email with Idea Task",
    "Title: Task f6",
    "Title: 1.117",
    "Title: Task f5",
    "Title: Task f12",
    "Title: Task f4",
    "Title: Task f3",
    "Title: 111.0.113",
    "Title: 111.0.115",
    "Title: Pages you view in incognito tabs won't stick around in your browser's history, cookie store, or search history after you've closed all of your incognito tabs. Any files you download or bookmarks you create will be kept.",
    "Title: Task f7",
    "Title: 1.116",
    "Title: 1.118",
    "Title: Going incognito doesn't hide your browsing from your employer, your internet service provider, or the websites you visit. ",
    "Title: 111.0.111"
)

If I print self.taskArray later, may be in didSelectRow delegate of UITableView, it prints like below:

(
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)",
    "Title: (null)"
)

I think this may be some thing related to NSManagedObjectContext, but don't know how to fix it.

Please help!

Khawar
  • 9,151
  • 9
  • 46
  • 67
  • When the `tasks` is empty you are clearing the `tasksArray` anyways. There is your problem. – Desdenova Apr 29 '14 at 06:56
  • @Desdenova, thanks for reply, emptying tasksArray is never called, it is called only for new request of data. BTW, I have also tried commenting line that is emptying tasksArray, but still not working. – Khawar Apr 29 '14 at 07:01
  • OK, something is clearly emptying the array. Try putting a breakpoint on the property and see what is accessing it. You should be able to backtrace from there. – Desdenova Apr 29 '14 at 07:07
  • 4
    Are the objects fetched on a secondary managed object context? If that context is deallocated, the objects continue to exist, but all their properties evaluate to `nil`. – Martin R Apr 29 '14 at 07:11
  • 1
    @Desdenova, tasksArray is not being empty from anywhere. The tasksArray.count is still displaying 34. It is the data which is being null. Also this is working perfect in Online mode, where objects are retrieved by RestKit. – Khawar Apr 29 '14 at 07:17
  • @MartinR, Thanks for hint, it is working now. I have changed Magical Record's method to fetch with defaultContext and it is working now. – Khawar Apr 29 '14 at 07:26

6 Answers6

20

The problem is that (as I wrote in a comment) that the objects are fetched on a background thread, but used on the main (UI) thread. Managed objects can only "live" in the context that they were created in. If the context is deallocated, the objects still exist, but the property accessor methods return just nil.

Possible solutions:

  • Fetch the objects on the main thread.
  • Use

    NSManagedObject *copy = [[mainContext objectWithID:[object objectID]];
    

    to "copy" the objects from the background context to the main context. (Perhaps MagicalRecord has a convenience method.)

  • Instead of fetching managed objects, set

    [fetchRequest setResultType:NSDictionaryResultType];
    [fetchRequest setPropertiesToFetch:@[@"title", ...]];
    

    to fetch an array of dictionaries with the attributes you are interested in.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • If the object can only "live" in the context that were created can you just make the findAll in the CoreData default context (**[NSManagedObjectContext MR_defaultContext]**) ? It did seem to fix my problem since this context is never deallocated. `NSArray *tasks = [self MR_findAllWithPredicate:[NSPredicate predicateWithFormat:@"due_date = nil AND user_id = %@", [DBUsers currentUser].id] inContext:[NSManagedObjectContext MR_defaultContext]]`; – Ciprian Rarau Nov 11 '14 at 02:01
  • 1
    It turns out that you should never use NSManagedObjects in other threads even if the context is not deallocated. I kept on getting random crashes while accessing some properties on other threads. Now I'm passing IDs and retrieving the NSManagedObjects where I need them. In another case I've made my own object detached from CoreData. – Ciprian Rarau Nov 13 '14 at 02:46
  • I have no idea how i missed this. Was stuck on this for 2 weeks.. grrr – Skywalker Apr 28 '21 at 05:39
3

Finally found the reason for Null objects. I was calling fetching in background. So Magical Records creates a new Managed Object Context specific for that thread, in NSManagedObject+MagicalFinders.m

+ (NSManagedObjectContext *) MR_contextForCurrentThread;
{
    if ([NSThread isMainThread])
    {
        return [self MR_defaultContext];
    }
    else
    {
        NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary];
        NSManagedObjectContext *threadContext = [threadDict objectForKey:kMagicalRecordManagedObjectContextKey];
        if (threadContext == nil)
        {
            threadContext = [self MR_contextWithParent:[NSManagedObjectContext MR_defaultContext]];
            [threadDict setObject:threadContext forKey:kMagicalRecordManagedObjectContextKey];
        }
        return threadContext;
    }
}

So I needed to copy objects from background thread context to main thread context. I finally found the solution here and created my method like this.

+ (void)backgroundFetchWithPredicate:(NSPredicate *)predicate completion:(void(^)(NSArray *, NSError *))completion {
    NSManagedObjectContext *privateContext = [NSManagedObjectContext MR_context];
    [privateContext performBlock:^{
        NSArray *privateObjects = [self MR_findAllWithPredicate:predicate inContext:privateContext];
        NSArray *privateObjectIDs = [privateObjects valueForKey:@"objectID"];
        // Return to our main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            NSPredicate *mainPredicate = [NSPredicate predicateWithFormat:@"self IN %@", privateObjectIDs];
            NSArray *finalResults = [self MR_findAllWithPredicate:mainPredicate];
            completion(finalResults, nil);
        });
    }];
}

Now I just need to call it like this:

[self backgroundFetchWithPredicate:predicate completion:^(NSArray *results, NSError *error) {
            completionHandler(results, error);
        }];
Community
  • 1
  • 1
Khawar
  • 9,151
  • 9
  • 46
  • 67
0

I guess it is because you are directly assigning the value to the array. Try assigning value by using mutablecopy

NSArray *tasks = [[self MR_findAllWithPredicate:[NSPredicate predicateWithFormat:@"due_date = nil AND user_id = %@", [DBUsers currentUser].id]] mutableCopy];
Prashanth Rajagopalan
  • 718
  • 1
  • 10
  • 27
  • thanks for reply, I have tried your solution, but `mutableCopy` is not working even. Results still become Null – Khawar Apr 29 '14 at 07:00
0

I've found several weak points in your code, try to work them out:

  1. You never use pageNo parameter in your + (void)getIdeasTasksWithPageNo:(int)pageNo completionHandler:(void (^)(NSArray *, NSError *))completionHandler

  2. You always pass nil for error parameter in the method above

  3. What is the purpose of removing objects from array [self.tasksArray removeAllObjects]; ? (firstly try to comment this line maybe this is the reason)

Stas
  • 9,925
  • 9
  • 42
  • 77
  • @Stats, thanks for you reply. Actually I have mentioned the full code here. pageNo is used for pagination. In original code if there is an error, I call completionHandler(nil, error); [self.tasksArray removeAllObjects] is used if user again call for data, we need to remove previously data from Array and add new fetched data, if I don't remove, it shows duplicate records in UITableView. – Khawar Apr 29 '14 at 09:30
  • The issue in my question is I am calling method in background thread. And data in background thread is fetched using temporary Managed Object Context. So when I come back to Main thread, that temporary Managed Object Context becomes null, so that all objects also become null. Now I am looking for some way to copy objects from temporary Managed Object Context to my Default Managed Object (using in main thread). – Khawar Apr 29 '14 at 09:31
0

First, check if you are using CoreData in a multi-thread circumstance, if you do, you may need to add some dispatch queue code to make sure your CoreData code running on only one thread. Second, call some other methods of your result array to see if the NSManagedObject is just in a fault status, calling some methods of it will make it fetched.

CarmeloS
  • 7,868
  • 8
  • 56
  • 103
0

Also, perform one prophylactic check on the implementation of your NSManagedObject class with unexpected Null value in fields. In the .m file of NSManagedObject class, make sure that @dynamic is executed for the fields, and not for example, for some reason, by mistake @synthesize.

// NullFieldsCoreDataObject.h
@interface NullFieldsCoreDataObject : NSManagedObject
@property (nonatomic, retain) NSString * nullField;
@property (nonatomic, retain) NSString * anotherOneNullField;
@property (nonatomic, retain) NSNumber * andAnotherOneNullField;
@end

// NullFieldsCoreDataObject.m
#import "NullFieldsCoreDataObject.h"

@implementation NullFieldsCoreDataObject
// wrong:
//@synthesize nullField, anotherOneNullField, andAnotherOneNullField;
// correctly:
@dynamic nullField, anotherOneNullField, andAnotherOneNullField;
@end
CAHbl463
  • 102
  • 7